ignotum 0.0.6 → 0.0.8

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 (54) hide show
  1. package/README.md +85 -101
  2. package/dist/cli/bin.mjs +1487 -168
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-DtX8qPrq.js → api-BTeMI5tx.js} +27 -3
  5. package/dist/runtime/api-BTeMI5tx.js.map +1 -0
  6. package/dist/runtime/{api-D2bsAz4T.d.ts → api-D9lV-5av.d.ts} +7 -10
  7. package/dist/runtime/client.d.ts +27 -3
  8. package/dist/runtime/client.js +468 -68
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{descriptor-t6BOEGw9-BTHR-ZMv.js → descriptor-C5VA9qRl-C338iC6l.js} +63 -5
  11. package/dist/runtime/descriptor-C5VA9qRl-C338iC6l.js.map +1 -0
  12. package/dist/runtime/file-BXf63ulU.js +166 -0
  13. package/dist/runtime/file-BXf63ulU.js.map +1 -0
  14. package/dist/runtime/{result-BgcHn25t.d.ts → id-Btwac71X-DGj0DQuu.d.ts} +56 -4
  15. package/dist/runtime/{index-Dl_VQGmA.d.ts → index-CF04_Dps.d.ts} +96 -86
  16. package/dist/runtime/internal/api.d.ts +2 -2
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +25 -8
  19. package/dist/runtime/internal/host.js +209 -15
  20. package/dist/runtime/internal/host.js.map +1 -1
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/pagination-BNFhAjns.d.ts +1 -0
  26. package/dist/runtime/pagination-DrOowBve-Bnipd34u.d.ts +84 -0
  27. package/dist/runtime/{schema-B-jMZEbs.js → schema-DJfwfq87.js} +108 -30
  28. package/dist/runtime/schema-DJfwfq87.js.map +1 -0
  29. package/dist/runtime/server.d.ts +3 -3
  30. package/dist/runtime/server.js +2 -2
  31. package/dist/runtime/server.js.map +1 -1
  32. package/dist/runtime/sync-mIXn9eKv.d.ts +8 -0
  33. package/package.json +2 -2
  34. package/src/cli/agent-files.ts +56 -13
  35. package/src/cli/app-configuration.ts +4 -1
  36. package/src/cli/build/server.ts +83 -6
  37. package/src/cli/new-app.ts +15 -0
  38. package/src/client/files.ts +168 -0
  39. package/src/client/hooks.ts +166 -17
  40. package/src/client/index.ts +9 -1
  41. package/src/client/sync.ts +137 -21
  42. package/src/dev-runtime/database.ts +621 -78
  43. package/src/dev-runtime/files.ts +338 -0
  44. package/src/dev-runtime/functions.ts +14 -6
  45. package/src/dev-runtime/migrations.ts +44 -0
  46. package/src/dev-runtime/sync.ts +123 -5
  47. package/src/internal/api.ts +16 -1
  48. package/src/server/index.ts +8 -1
  49. package/dist/runtime/api-DtX8qPrq.js.map +0 -1
  50. package/dist/runtime/descriptor-t6BOEGw9-BTHR-ZMv.js.map +0 -1
  51. package/dist/runtime/id-Btwac71X-B9OeBzvq.d.ts +0 -9
  52. package/dist/runtime/id-D570vudg.js +0 -26
  53. package/dist/runtime/id-D570vudg.js.map +0 -1
  54. package/dist/runtime/schema-B-jMZEbs.js.map +0 -1
package/dist/cli/bin.mjs CHANGED
@@ -11,7 +11,7 @@ import * as Path$3 from "effect/Path";
11
11
  import { Path, TypeId } from "effect/Path";
12
12
  import * as Predicate$1 from "effect/Predicate";
13
13
  import * as Sink from "effect/Sink";
14
- import * as Stream from "effect/Stream";
14
+ import * as Stream$1 from "effect/Stream";
15
15
  import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
16
16
  import { ChildProcessSpawner, ExitCode, ProcessId, make, makeHandle } from "effect/unstable/process/ChildProcessSpawner";
17
17
  import * as NodeChildProcess from "node:child_process";
@@ -69,7 +69,7 @@ import * as RcRef from "effect/RcRef";
69
69
  import * as Terminal$1 from "effect/Terminal";
70
70
  import * as readline from "node:readline";
71
71
  import * as Runtime from "effect/Runtime";
72
- import { Array as Array$1, Brand, Cause, Config, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Ref, Result, Schedule, Schema, Semaphore, String as String$1, Terminal } from "effect";
72
+ import { Array as Array$1, Brand, Cause, Config, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Terminal } from "effect";
73
73
  import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
74
74
  import { customAlphabet } from "nanoid";
75
75
  import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerRequest as HttpServerRequest$1, HttpServerResponse } from "effect/unstable/http";
@@ -251,7 +251,7 @@ const pullIntoWritable = (options) => options.pull.pipe(Effect$1.flatMap((chunk)
251
251
  * @category constructors
252
252
  * @since 4.0.0
253
253
  */
254
- const fromReadable = (options) => Stream.fromChannel(fromReadableChannel(options));
254
+ const fromReadable = (options) => Stream$1.fromChannel(fromReadableChannel(options));
255
255
  /**
256
256
  * Creates a `Channel` that pulls chunks from a Node readable stream, mapping
257
257
  * errors with `onError` and destroying the readable on completion unless
@@ -406,7 +406,7 @@ var StreamAdapter = class extends Readable {
406
406
  constructor(context, stream) {
407
407
  super({});
408
408
  this.readLatch = Latch.makeUnsafe(false);
409
- this.fiber = Stream.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
409
+ this.fiber = Stream$1.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
410
410
  this.readLatch.closeUnsafe();
411
411
  for (let i = 0; i < chunk.length; i++) {
412
412
  const item = chunk[i];
@@ -462,7 +462,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
462
462
  ...options.env
463
463
  } : options.env;
464
464
  };
465
- const inputToStdioOption = (input) => Stream.isStream(input) ? "pipe" : input;
465
+ const inputToStdioOption = (input) => Stream$1.isStream(input) ? "pipe" : input;
466
466
  const outputToStdioOption = (input) => Sink.isSink(input) ? "pipe" : input;
467
467
  const resolveStdinOption = (options) => {
468
468
  const defaultConfig = {
@@ -475,7 +475,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
475
475
  ...defaultConfig,
476
476
  stream: options.stdin
477
477
  };
478
- if (Stream.isStream(options.stdin)) return {
478
+ if (Stream$1.isStream(options.stdin)) return {
479
479
  ...defaultConfig,
480
480
  stream: options.stdin
481
481
  };
@@ -519,7 +519,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
519
519
  const setupAdditionalFds = Effect$1.fnUntraced(function* (command, childProcess, additionalFds) {
520
520
  if (additionalFds.length === 0) return {
521
521
  getInputFd: () => Sink.drain,
522
- getOutputFd: () => Stream.empty
522
+ getOutputFd: () => Stream$1.empty
523
523
  };
524
524
  const inputSinks = /* @__PURE__ */ new Map();
525
525
  const outputStreams = /* @__PURE__ */ new Map();
@@ -532,12 +532,12 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
532
532
  evaluate: () => nodeStream,
533
533
  onError: (error) => toPlatformError(`fromWritable(fd${fd})`, toError(error), command)
534
534
  });
535
- if (config.stream) yield* Effect$1.forkScoped(Stream.run(config.stream, sink));
535
+ if (config.stream) yield* Effect$1.forkScoped(Stream$1.run(config.stream, sink));
536
536
  inputSinks.set(fd, sink);
537
537
  break;
538
538
  }
539
539
  case "output": {
540
- let stream = Stream.empty;
540
+ let stream = Stream$1.empty;
541
541
  if (nodeStream && "read" in nodeStream) {
542
542
  const passThrough = new PassThrough();
543
543
  nodeStream.on("error", (error) => passThrough.destroy(error));
@@ -547,7 +547,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
547
547
  onError: (error) => toPlatformError(`fromReadable(fd${fd})`, toError(error), command)
548
548
  });
549
549
  }
550
- if (config.sink) stream = Stream.transduce(stream, config.sink);
550
+ if (config.sink) stream = Stream$1.transduce(stream, config.sink);
551
551
  outputStreams.set(fd, stream);
552
552
  break;
553
553
  }
@@ -555,7 +555,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
555
555
  }
556
556
  return {
557
557
  getInputFd: (fd) => inputSinks.get(fd) ?? Sink.drain,
558
- getOutputFd: (fd) => outputStreams.get(fd) ?? Stream.empty
558
+ getOutputFd: (fd) => outputStreams.get(fd) ?? Stream$1.empty
559
559
  };
560
560
  });
561
561
  const setupChildStdin = (command, childProcess, config) => Effect$1.suspend(() => {
@@ -566,7 +566,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
566
566
  endOnDone: config.endOnDone,
567
567
  encoding: config.encoding
568
568
  });
569
- if (Stream.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream.run(config.stream, sink)), sink);
569
+ if (Stream$1.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream$1.run(config.stream, sink)), sink);
570
570
  return Effect$1.succeed(sink);
571
571
  });
572
572
  const setupChildOutputStreams = (command, childProcess, stdoutConfig, stderrConfig) => {
@@ -578,7 +578,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
578
578
  evaluate: () => passThrough,
579
579
  onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
580
580
  });
581
- })() : Stream.empty;
581
+ })() : Stream$1.empty;
582
582
  let stderr = childProcess.stderr ? (() => {
583
583
  const passThrough = new PassThrough();
584
584
  childProcess.stderr.on("error", (error) => passThrough.destroy(error));
@@ -587,10 +587,10 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
587
587
  evaluate: () => passThrough,
588
588
  onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
589
589
  });
590
- })() : Stream.empty;
591
- if (Sink.isSink(stdoutConfig.stream)) stdout = Stream.transduce(stdout, stdoutConfig.stream);
592
- if (Sink.isSink(stderrConfig.stream)) stderr = Stream.transduce(stderr, stderrConfig.stream);
593
- const all = Stream.merge(stdout, stderr);
590
+ })() : Stream$1.empty;
591
+ if (Sink.isSink(stdoutConfig.stream)) stdout = Stream$1.transduce(stdout, stdoutConfig.stream);
592
+ if (Sink.isSink(stderrConfig.stream)) stderr = Stream$1.transduce(stderr, stderrConfig.stream);
593
+ const all = Stream$1.merge(stdout, stderr);
594
594
  return {
595
595
  stdout,
596
596
  stderr,
@@ -744,7 +744,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
744
744
  const command = pipeline[i];
745
745
  const options = pipeOptions[i] ?? {};
746
746
  const stdinConfig = resolveStdinOption(command.options);
747
- const sourceStream = Stream.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
747
+ const sourceStream = Stream$1.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
748
748
  const toOption = options.to ?? "stdin";
749
749
  if (toOption === "stdin") handles.push(yield* spawnCommand(ChildProcess$1.make(command.command, command.args, {
750
750
  ...command.options,
@@ -4759,7 +4759,7 @@ const utimes = /*#__PURE__*/ (() => {
4759
4759
  const nodeUtimes = /*#__PURE__*/ effectify(NFS.utimes, /*#__PURE__*/ handleErrnoException("FileSystem", "utime"), /*#__PURE__*/ handleBadArgument("utime"));
4760
4760
  return (path, atime, mtime) => nodeUtimes(path, atime, mtime);
4761
4761
  })();
4762
- const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
4762
+ const watchNode = (path, options) => Stream$1.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
4763
4763
  const watcher = NFS.watch(path, { recursive: options?.recursive ?? false }, (event, path) => {
4764
4764
  if (!path) return;
4765
4765
  switch (event) {
@@ -4797,7 +4797,7 @@ const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquire
4797
4797
  });
4798
4798
  return watcher;
4799
4799
  }), (watcher) => Effect$1.sync(() => watcher.close())));
4800
- const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream.unwrap);
4800
+ const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream$1.unwrap);
4801
4801
  const writeFile$1 = (path, data, options) => Effect$1.callback((resume, signal) => {
4802
4802
  try {
4803
4803
  NFS.writeFile(path, data, {
@@ -28742,7 +28742,7 @@ const stream = (source, headers) => Multipart.makeConfig(headers).pipe(Effect$1.
28742
28742
  return parser;
28743
28743
  },
28744
28744
  onError: (error) => convertError(error)
28745
- })), Stream.unwrap, Stream.map(convertPart));
28745
+ })), Stream$1.unwrap, Stream$1.map(convertPart));
28746
28746
  /**
28747
28747
  * Parses multipart data from a Node readable request body and persists file
28748
28748
  * parts using the current `FileSystem`, `Path`, and `Scope` services.
@@ -29305,7 +29305,7 @@ const handleResponse = (request, response) => {
29305
29305
  nodeResponse.writeHead(response.status, headers);
29306
29306
  const drainLatch = Latch.makeUnsafe();
29307
29307
  nodeResponse.on("drain", () => drainLatch.openUnsafe());
29308
- return body.stream.pipe(Stream.orDie, Stream.runForEachArray((array) => {
29308
+ return body.stream.pipe(Stream$1.orDie, Stream$1.runForEachArray((array) => {
29309
29309
  const chunk = array.length > 1 ? Buffer.concat(array) : array[0];
29310
29310
  if (nodeResponse.write(chunk)) return Effect$1.void;
29311
29311
  drainLatch.closeUnsafe();
@@ -29407,6 +29407,124 @@ const DevDatabaseLockId = defineId("lock", "ignotum/dev/DatabaseLockId");
29407
29407
  const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
29408
29408
  const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
29409
29409
  const AppStateRevision = Schema.Natural.pipe(Schema.brand("ignotum/hosted/AppStateRevision"));
29410
+ const FileFormat = Schema.Literals([
29411
+ "jpeg",
29412
+ "png",
29413
+ "webp",
29414
+ "avif",
29415
+ "gif"
29416
+ ]);
29417
+ const fileMimeTypes = {
29418
+ avif: "image/avif",
29419
+ gif: "image/gif",
29420
+ jpeg: "image/jpeg",
29421
+ png: "image/png",
29422
+ webp: "image/webp"
29423
+ };
29424
+ const fileLimits = {
29425
+ activeGrantsPerConnection: 1024,
29426
+ distinctFilesPerQuerySnapshot: 256,
29427
+ fileBytes: 10485760,
29428
+ filesPerMutation: 8,
29429
+ filenameBytes: 255,
29430
+ mutationBytes: 26214400,
29431
+ preparationLifetimeMillis: 9e5,
29432
+ stagedBytesPerApp: 104857600
29433
+ };
29434
+ const FileId = Object.assign(Schema.String.pipe(Schema.check(Schema.isPattern(/^file_[0-9A-Za-z_-]{20,128}$/)), Schema.brand("ignotum/file/FileId")), { idPrefix: "file" });
29435
+ const FileUploadToken = defineId("upload", "ignotum/file/FileUploadToken");
29436
+ const FileGrantToken = defineId("grant", "ignotum/file/FileGrantToken");
29437
+ const FileValueTypeId = Symbol.for("ignotum/file/FileValue");
29438
+ const FileGrantTypeId = Symbol.for("ignotum/file/FileGrant");
29439
+ const isFileValue = (value) => Predicate.isObject(value) && Predicate.hasProperty(value, FileValueTypeId) && Predicate.isObject(value[FileValueTypeId]) && Predicate.hasProperty(value[FileValueTypeId], "id") && Schema.is(FileId)(value[FileValueTypeId].id) && Predicate.hasProperty(value, "format") && Schema.is(FileFormat)(value.format) && Predicate.hasProperty(value, "name") && Predicate.isString(value.name) && Predicate.hasProperty(value, "size") && Predicate.isNumber(value.size) && Predicate.hasProperty(value, "mimeType") && value.mimeType === fileMimeTypes[value.format];
29440
+ const buildFileValue = (id, metadata, grantUrl) => {
29441
+ const value = {
29442
+ format: metadata.format,
29443
+ mimeType: fileMimeTypes[metadata.format],
29444
+ name: metadata.name,
29445
+ size: metadata.size
29446
+ };
29447
+ Object.defineProperty(value, FileValueTypeId, {
29448
+ configurable: false,
29449
+ enumerable: false,
29450
+ value: Object.freeze({ id }),
29451
+ writable: false
29452
+ });
29453
+ if (grantUrl !== void 0) Object.defineProperty(value, FileGrantTypeId, {
29454
+ configurable: false,
29455
+ enumerable: false,
29456
+ value: grantUrl,
29457
+ writable: false
29458
+ });
29459
+ return Object.freeze(value);
29460
+ };
29461
+ const makeFileValue = (id, metadata) => buildFileValue(id, metadata);
29462
+ const fileIdOf = (value) => value[FileValueTypeId].id;
29463
+ const EncodedFileValue = Schema.Struct({
29464
+ $ignotum: Schema.Literal("file"),
29465
+ id: FileId,
29466
+ format: FileFormat,
29467
+ name: Schema.String,
29468
+ size: Schema.Int.check(Schema.isBetween({
29469
+ minimum: 0,
29470
+ maximum: fileLimits.fileBytes
29471
+ }))
29472
+ });
29473
+ const RuntimeFileMetadata = Schema.Struct({
29474
+ id: FileId,
29475
+ format: FileFormat,
29476
+ name: Schema.String,
29477
+ size: Schema.Int.check(Schema.isBetween({
29478
+ minimum: 0,
29479
+ maximum: fileLimits.fileBytes
29480
+ }))
29481
+ });
29482
+ const FilePath = Schema.Array(Schema.Union([Schema.String, Schema.Natural]));
29483
+ const RuntimeFileOccurrence = Schema.Struct({
29484
+ path: FilePath,
29485
+ file: RuntimeFileMetadata
29486
+ });
29487
+ const DecodedFileValue = Schema.declare(isFileValue, { title: "FileValue" });
29488
+ const FileValue = EncodedFileValue.pipe(Schema.decodeTo(DecodedFileValue, {
29489
+ decode: SchemaGetter.transform((encoded) => makeFileValue(encoded.id, {
29490
+ format: encoded.format,
29491
+ name: encoded.name,
29492
+ size: encoded.size
29493
+ })),
29494
+ encode: SchemaGetter.transform((value) => ({
29495
+ $ignotum: "file",
29496
+ format: value.format,
29497
+ id: fileIdOf(value),
29498
+ name: value.name,
29499
+ size: value.size
29500
+ }))
29501
+ }));
29502
+ const collectEncodedFileOccurrences = (value, path, occurrences) => {
29503
+ if (Schema.is(EncodedFileValue)(value)) {
29504
+ const file = Schema.decodeUnknownSync(EncodedFileValue)(value);
29505
+ occurrences.push({
29506
+ path,
29507
+ file: {
29508
+ format: file.format,
29509
+ id: file.id,
29510
+ name: file.name,
29511
+ size: file.size
29512
+ }
29513
+ });
29514
+ return;
29515
+ }
29516
+ if (globalThis.Array.isArray(value)) {
29517
+ for (const [index, child] of value.entries()) collectEncodedFileOccurrences(child, [...path, index], occurrences);
29518
+ return;
29519
+ }
29520
+ if (!Schema.is(Schema.JsonObject)(value)) return;
29521
+ for (const [key, child] of Object.entries(value)) collectEncodedFileOccurrences(child, [...path, key], occurrences);
29522
+ };
29523
+ const encodedFileOccurrencesOf = (value) => {
29524
+ const occurrences = [];
29525
+ collectEncodedFileOccurrences(value, [], occurrences);
29526
+ return occurrences;
29527
+ };
29410
29528
  //#endregion
29411
29529
  //#region ../contracts/dist/runtime/value.js
29412
29530
  const TransportValueSchema = Schema.suspend(() => Schema.Union([
@@ -29415,6 +29533,7 @@ const TransportValueSchema = Schema.suspend(() => Schema.Union([
29415
29533
  Schema.Finite,
29416
29534
  Schema.String,
29417
29535
  Schema.Date,
29536
+ FileValue,
29418
29537
  Schema.Array(TransportValueSchema),
29419
29538
  Schema.Record(Schema.String, Schema.UndefinedOr(TransportValueSchema))
29420
29539
  ]));
@@ -29424,6 +29543,7 @@ const collectDatePaths = (value, path, paths) => {
29424
29543
  paths.push(path);
29425
29544
  return;
29426
29545
  }
29546
+ if (isFileValue(value)) return;
29427
29547
  if (globalThis.Array.isArray(value)) {
29428
29548
  for (const [index, child] of value.entries()) collectDatePaths(child, [...path, index], paths);
29429
29549
  return;
@@ -29476,9 +29596,18 @@ const Invoke = Schema.Struct({
29476
29596
  function: FunctionAddress,
29477
29597
  args: Schema.Json
29478
29598
  });
29599
+ const PrepareMutation = Schema.Struct({
29600
+ type: Schema.Literal("PrepareMutation"),
29601
+ id: InvocationId,
29602
+ kind: Schema.Literal("Mutation"),
29603
+ function: FunctionAddress,
29604
+ args: Schema.Json,
29605
+ files: Schema.Array(RuntimeFileOccurrence)
29606
+ });
29479
29607
  const ClientMessage = Schema.Union([
29480
29608
  Subscribe,
29481
29609
  Unsubscribe,
29610
+ PrepareMutation,
29482
29611
  Invoke
29483
29612
  ]);
29484
29613
  const SubscriptionOperation = Schema.Struct({
@@ -29513,7 +29642,21 @@ const Snapshot = Schema.Struct({
29513
29642
  type: Schema.Literal("Snapshot"),
29514
29643
  id: SubscriptionId,
29515
29644
  result: WireResult,
29516
- revision: AppStateRevision
29645
+ revision: AppStateRevision,
29646
+ files: Schema.optional(Schema.Array(Schema.Struct({
29647
+ path: RuntimeFileOccurrence.fields.path,
29648
+ file: RuntimeFileMetadata,
29649
+ url: Schema.String
29650
+ })))
29651
+ });
29652
+ const MutationPrepared = Schema.Struct({
29653
+ type: Schema.Literal("MutationPrepared"),
29654
+ id: InvocationId,
29655
+ uploads: Schema.Array(Schema.Struct({
29656
+ path: RuntimeFileOccurrence.fields.path,
29657
+ file: RuntimeFileMetadata,
29658
+ url: Schema.optional(Schema.String)
29659
+ }))
29517
29660
  });
29518
29661
  const SyncResultSuccess = Schema.Struct({
29519
29662
  type: Schema.Literal("Result"),
@@ -29550,6 +29693,7 @@ const DeploymentChanged = Schema.Struct({
29550
29693
  const ServerMessage = Schema.Union([
29551
29694
  SyncHandshake,
29552
29695
  Snapshot,
29696
+ MutationPrepared,
29553
29697
  SyncResultSuccess,
29554
29698
  SyncResultFailure,
29555
29699
  ProtocolError,
@@ -29580,7 +29724,7 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
29580
29724
  message: Schema.String
29581
29725
  }) {};
29582
29726
  //#endregion
29583
- //#region ../contracts/dist/descriptor-t6BOEGw9.js
29727
+ //#region ../contracts/dist/descriptor-C5VA9qRl.js
29584
29728
  const LiteralValue = Schema.Union([
29585
29729
  Schema.String,
29586
29730
  Schema.Finite,
@@ -29597,6 +29741,17 @@ const ValueDescriptor = Schema.Union([
29597
29741
  }),
29598
29742
  Schema.Struct({ type: Schema.Literal("boolean") }),
29599
29743
  Schema.Struct({ type: Schema.Literal("date") }),
29744
+ Schema.Struct({
29745
+ type: Schema.Literal("file"),
29746
+ formats: Schema.Array(Schema.Literals([
29747
+ "jpeg",
29748
+ "png",
29749
+ "webp",
29750
+ "avif",
29751
+ "gif"
29752
+ ])),
29753
+ maxBytes: Schema.Int
29754
+ }),
29600
29755
  Schema.Struct({
29601
29756
  type: Schema.Literal("error"),
29602
29757
  tag: Schema.String,
@@ -29650,6 +29805,53 @@ Function$1.dual(2, (value, descriptor) => {
29650
29805
  });
29651
29806
  return value;
29652
29807
  });
29808
+ const getValueDescriptor = (value) => {
29809
+ if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
29810
+ return Schema.is(ValueDescriptor)(value[ValueDescriptorTypeId]) ? value[ValueDescriptorTypeId] : void 0;
29811
+ };
29812
+ const descriptorContainsFile = (descriptor) => {
29813
+ switch (descriptor.type) {
29814
+ case "file": return true;
29815
+ case "array":
29816
+ case "nullable":
29817
+ case "optional":
29818
+ case "record": return descriptorContainsFile(descriptor.value);
29819
+ case "error":
29820
+ case "object": return descriptor.fields.some((field) => descriptorContainsFile(field.value));
29821
+ case "union": return descriptor.members.some(descriptorContainsFile);
29822
+ default: return false;
29823
+ }
29824
+ };
29825
+ const descriptorFields = (fields) => {
29826
+ const entries = [];
29827
+ for (const name of Reflect.ownKeys(fields)) {
29828
+ if (!Predicate.isString(name)) throw new Error("Ignotum value objects only support string field names.");
29829
+ const field = fields[name];
29830
+ const value = field === void 0 ? void 0 : getValueDescriptor(field);
29831
+ if (value === void 0) throw new Error(`The field '${name}' must use an Ignotum values validator.`);
29832
+ entries.push({
29833
+ name,
29834
+ value
29835
+ });
29836
+ }
29837
+ return entries.sort((left, right) => left.name.localeCompare(right.name));
29838
+ };
29839
+ //#endregion
29840
+ //#region ../contracts/dist/versioned.js
29841
+ /** Registers the first revision of a long-lived format. */
29842
+ const initial = (schema) => schema;
29843
+ /** Adds one adjacent migration and keeps encoding on the new current revision. */
29844
+ const upgrade = (previous, current, migrate) => {
29845
+ const accepted = Schema.Union([previous, current]);
29846
+ const isCurrent = Schema.is(current);
29847
+ return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
29848
+ decode: SchemaGetter.transform((value) => {
29849
+ if (isCurrent(value)) return value;
29850
+ return migrate(value);
29851
+ }),
29852
+ encode: SchemaGetter.transform((value) => value)
29853
+ }));
29854
+ };
29653
29855
  //#endregion
29654
29856
  //#region ../contracts/dist/deployment.js
29655
29857
  const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
@@ -29683,15 +29885,15 @@ const ClientRoute = Schema.Struct({
29683
29885
  pathname: ClientPath,
29684
29886
  artifact: ArtifactReference
29685
29887
  });
29686
- const DeploymentInventory = Schema.Struct({
29888
+ const DeploymentInventory = initial(Schema.Struct({
29687
29889
  formatVersion: Schema.Literal(1),
29688
29890
  files: Schema.Array(ArtifactFile)
29689
- });
29690
- const ClientManifest = Schema.Struct({
29891
+ }));
29892
+ const ClientManifest = initial(Schema.Struct({
29691
29893
  formatVersion: Schema.Literal(1),
29692
29894
  shell: ArtifactReference,
29693
29895
  routes: Schema.Array(ClientRoute)
29694
- });
29896
+ }));
29695
29897
  const ClientRoutingRoute = Schema.Struct({
29696
29898
  pathname: ClientPath,
29697
29899
  artifact: ArtifactPath
@@ -29705,25 +29907,64 @@ const SchemaSnapshotField = Schema.Struct({
29705
29907
  name: Schema.String,
29706
29908
  value: ValueDescriptor
29707
29909
  });
29708
- const SchemaSnapshotTable = Schema.Struct({
29910
+ const SchemaSnapshotIndex = Schema.Struct({
29911
+ name: Schema.String,
29912
+ fields: Schema.Array(Schema.String)
29913
+ });
29914
+ const SchemaSnapshotTableV1 = Schema.Struct({
29709
29915
  name: Schema.String,
29710
29916
  fields: Schema.Array(SchemaSnapshotField)
29711
29917
  });
29712
- const SchemaSnapshot = Schema.Struct({
29918
+ const SchemaSnapshotV1 = Schema.Struct({
29713
29919
  formatVersion: Schema.Literal(1),
29920
+ tables: Schema.Array(SchemaSnapshotTableV1)
29921
+ });
29922
+ const SchemaSnapshotTable = Schema.Struct({
29923
+ name: Schema.String,
29924
+ fields: Schema.Array(SchemaSnapshotField),
29925
+ indexes: Schema.Array(SchemaSnapshotIndex)
29926
+ });
29927
+ const SchemaSnapshotV2 = Schema.Struct({
29928
+ formatVersion: Schema.Literal(2),
29714
29929
  tables: Schema.Array(SchemaSnapshotTable)
29715
29930
  });
29931
+ const SchemaSnapshot = upgrade(initial(SchemaSnapshotV1), SchemaSnapshotV2, (snapshot) => ({
29932
+ formatVersion: 2,
29933
+ tables: snapshot.tables.map((table) => ({
29934
+ ...table,
29935
+ indexes: []
29936
+ }))
29937
+ }));
29938
+ const ServerFunctionArtifactV1 = Schema.Struct({
29939
+ address: FunctionAddress,
29940
+ kind: FunctionKind,
29941
+ bundle: ArtifactReference,
29942
+ sourceMap: ArtifactReference
29943
+ });
29716
29944
  const ServerFunctionArtifact = Schema.Struct({
29717
29945
  address: FunctionAddress,
29718
29946
  kind: FunctionKind,
29947
+ args: Schema.optional(ValueDescriptor),
29948
+ returns: Schema.optional(ValueDescriptor),
29949
+ errors: Schema.optional(ValueDescriptor),
29719
29950
  bundle: ArtifactReference,
29720
29951
  sourceMap: ArtifactReference
29721
29952
  });
29722
- const ServerBuildManifest = Schema.Struct({
29953
+ const ServerBuildManifestV1 = Schema.Struct({
29723
29954
  formatVersion: Schema.Literal(1),
29724
29955
  schema: ArtifactReference,
29956
+ functions: Schema.Array(ServerFunctionArtifactV1)
29957
+ });
29958
+ const ServerBuildManifestV2 = Schema.Struct({
29959
+ formatVersion: Schema.Literal(2),
29960
+ schema: ArtifactReference,
29725
29961
  functions: Schema.Array(ServerFunctionArtifact)
29726
29962
  });
29963
+ const ServerBuildManifest = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
29964
+ formatVersion: 2,
29965
+ schema: manifest.schema,
29966
+ functions: manifest.functions
29967
+ }));
29727
29968
  const deploymentInventoryPath = ArtifactPath.make("inventory.json");
29728
29969
  const clientManifestPath = ArtifactPath.make("client/manifest.json");
29729
29970
  const clientShellPath = ArtifactPath.make("client/shell.html");
@@ -29839,7 +30080,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
29839
30080
  };
29840
30081
  //#endregion
29841
30082
  //#region package.json
29842
- var version = "0.0.6";
30083
+ var version = "0.0.8";
29843
30084
  //#endregion
29844
30085
  //#region src/cli/codegen.ts
29845
30086
  const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
@@ -30085,12 +30326,12 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
30085
30326
  }));
30086
30327
  //#endregion
30087
30328
  //#region ../contracts/dist/json.js
30088
- const encodeScalar = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
30329
+ const encodeScalar$1 = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
30089
30330
  const encodeCanonicalJson = (value) => {
30090
- if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar(value);
30331
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar$1(value);
30091
30332
  if (Predicate.isObject(value)) return `{${Array$1.map(Array$1.sort(String$1.Order)(Object.keys(value)), (key) => {
30092
30333
  const field = Schema.decodeUnknownSync(Schema.Json)(value[key]);
30093
- return `${encodeScalar(key)}:${encodeCanonicalJson(field)}`;
30334
+ return `${encodeScalar$1(key)}:${encodeCanonicalJson(field)}`;
30094
30335
  }).join(",")}}`;
30095
30336
  return `[${Schema.decodeUnknownSync(Schema.Array(Schema.Json))(value).map(encodeCanonicalJson).join(",")}]`;
30096
30337
  };
@@ -30173,6 +30414,58 @@ const documentNotFound = (table, id) => Brand.nominal()({
30173
30414
  id
30174
30415
  });
30175
30416
  //#endregion
30417
+ //#region ../contracts/dist/runtime/pagination.js
30418
+ const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
30419
+ const PaginationPageSize = Schema.Int.check(Schema.isBetween({
30420
+ minimum: 1,
30421
+ maximum: 1e3
30422
+ })).pipe(Schema.brand("ignotum/runtime/PaginationPageSize"));
30423
+ Schema.Struct({
30424
+ cursor: Schema.NullOr(PaginationCursor),
30425
+ pageSize: PaginationPageSize
30426
+ });
30427
+ const TablePosition = Schema.Struct({
30428
+ type: Schema.Literal("Table"),
30429
+ createdAt: Schema.Int,
30430
+ id: Schema.String
30431
+ });
30432
+ const IndexPosition = Schema.Struct({
30433
+ type: Schema.Literal("Index"),
30434
+ key: Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"))
30435
+ });
30436
+ const PaginationPosition = Schema.Union([TablePosition, IndexPosition]);
30437
+ const CursorPayload = initial(Schema.Struct({
30438
+ version: Schema.Literal(1),
30439
+ query: Schema.String,
30440
+ position: PaginationPosition
30441
+ }));
30442
+ const CursorPayloadJson = Schema.fromJsonString(CursorPayload);
30443
+ const paginationQueryIdentity = (tableId, index, lower, upper, order) => encodeCanonicalJson([
30444
+ tableId,
30445
+ index,
30446
+ lower ?? null,
30447
+ upper ?? null,
30448
+ order
30449
+ ]);
30450
+ const encodePaginationCursor = (query, position) => PaginationCursor.make(Encoding.encodeBase64Url(Schema.encodeSync(CursorPayloadJson)({
30451
+ version: 1,
30452
+ query,
30453
+ position
30454
+ })));
30455
+ const decodePaginationCursor = (cursor) => Schema.decodeSync(CursorPayloadJson)(Result.getOrThrow(Encoding.decodeBase64UrlString(cursor)));
30456
+ Schema.Union([
30457
+ Schema.String,
30458
+ Schema.Finite,
30459
+ Schema.Boolean
30460
+ ]);
30461
+ const schemaIndexLimits = {
30462
+ fieldsPerIndex: 8,
30463
+ indexesPerSchema: 128,
30464
+ indexesPerTable: 16,
30465
+ keyBytes: 4096,
30466
+ nameBytes: 64
30467
+ };
30468
+ //#endregion
30176
30469
  //#region ../contracts/dist/runtime/schema.js
30177
30470
  const FunctionSchemaTypeId = Symbol.for("ignotum/runtime/schema/FunctionSchema");
30178
30471
  const getFunctionSchema = (value) => value[FunctionSchemaTypeId];
@@ -30472,27 +30765,42 @@ const readArtifactDirectory = Effect.fn("Deployment.readArtifactDirectory")(func
30472
30765
  const makeSchemaSnapshot = (schema) => {
30473
30766
  const definition = schema[SchemaDefinitionTypeId];
30474
30767
  return {
30475
- formatVersion: 1,
30768
+ formatVersion: 2,
30476
30769
  tables: Array$1.map(Array$1.sort(String$1.Order)(Object.keys(definition)), (name) => {
30477
30770
  const table = definition[name];
30478
30771
  if (table === void 0 || table.descriptor.type !== "object") throw new Error(`The table '${name}' does not have a canonical Ignotum descriptor.`);
30772
+ const indexes = Object.values(table.indexes).map((index) => ({
30773
+ name: index.name,
30774
+ fields: globalThis.Array.from(index.fields)
30775
+ })).sort((left, right) => left.name.localeCompare(right.name));
30479
30776
  return {
30480
30777
  name,
30481
- fields: table.descriptor.fields
30778
+ fields: table.descriptor.fields,
30779
+ indexes
30482
30780
  };
30483
30781
  })
30484
30782
  };
30485
30783
  };
30486
30784
  const encodeSchemaSnapshot = (snapshot) => `${encodeCanonical(SchemaSnapshot, snapshot)}\n`;
30487
- Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot, previous) === encodeCanonical(SchemaSnapshot, next));
30488
- //#endregion
30489
- //#region src/cli/app-configuration.ts
30490
- const AppConfiguration = Schema.Struct({
30785
+ Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot, {
30786
+ ...previous,
30787
+ tables: previous.tables.map((table) => ({
30788
+ ...table,
30789
+ indexes: []
30790
+ }))
30791
+ }) === encodeCanonical(SchemaSnapshot, {
30792
+ ...next,
30793
+ tables: next.tables.map((table) => ({
30794
+ ...table,
30795
+ indexes: []
30796
+ }))
30797
+ }));
30798
+ const AppConfiguration = initial(Schema.Struct({
30491
30799
  formatVersion: Schema.Literal(1),
30492
30800
  appId: AppId,
30493
30801
  slug: AppSlug,
30494
30802
  apiUrl: Schema.URLFromString
30495
- });
30803
+ }));
30496
30804
  var AppConfigurationInvalid = class extends Schema.TaggedError()("AppConfigurationInvalid", {
30497
30805
  cause: Schema.optional(Schema.Defect()),
30498
30806
  message: Schema.String
@@ -30965,8 +31273,8 @@ var InvalidPublicFile = class extends Schema.TaggedError()("InvalidPublicFile",
30965
31273
  path: Schema.String
30966
31274
  }) {};
30967
31275
  const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
30968
- const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
30969
- const asciiAt = (bytes, value, offset = 0) => startsWith(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
31276
+ const startsWith$1 = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
31277
+ const asciiAt = (bytes, value, offset = 0) => startsWith$1(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
30970
31278
  const hasValidSignature = (extension, bytes) => {
30971
31279
  switch (extension) {
30972
31280
  case ".avif": {
@@ -30976,20 +31284,20 @@ const hasValidSignature = (extension, bytes) => {
30976
31284
  return false;
30977
31285
  }
30978
31286
  case ".gif": return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
30979
- case ".ico": return startsWith(bytes, [
31287
+ case ".ico": return startsWith$1(bytes, [
30980
31288
  0,
30981
31289
  0,
30982
31290
  1,
30983
31291
  0
30984
31292
  ]);
30985
31293
  case ".jpeg":
30986
- case ".jpg": return startsWith(bytes, [
31294
+ case ".jpg": return startsWith$1(bytes, [
30987
31295
  255,
30988
31296
  216,
30989
31297
  255
30990
31298
  ]);
30991
31299
  case ".pdf": return asciiAt(bytes, "%PDF-");
30992
- case ".png": return startsWith(bytes, [
31300
+ case ".png": return startsWith$1(bytes, [
30993
31301
  137,
30994
31302
  80,
30995
31303
  78,
@@ -31163,10 +31471,13 @@ const hostModuleId = "ignotum:host";
31163
31471
  const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.String));
31164
31472
  const DiscoveredServerFunction = Schema.Struct({
31165
31473
  address: FunctionAddress,
31474
+ args: Schema.optional(ValueDescriptor),
31475
+ errors: Schema.optional(ValueDescriptor),
31166
31476
  exportName: Schema.String,
31167
31477
  kind: Schema.Literals(["Mutation", "Query"]),
31168
31478
  moduleName: Schema.String,
31169
- modulePath: Schema.String
31479
+ modulePath: Schema.String,
31480
+ returns: Schema.optional(ValueDescriptor)
31170
31481
  });
31171
31482
  const ServerDiscoveryManifest = Schema.Struct({
31172
31483
  functions: Schema.Array(DiscoveredServerFunction),
@@ -31250,12 +31561,50 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
31250
31561
  });
31251
31562
  continue;
31252
31563
  }
31253
- functions.push({
31564
+ const args = inspected.definition.args === void 0 ? void 0 : {
31565
+ type: "object",
31566
+ fields: descriptorFields(inspected.definition.args)
31567
+ };
31568
+ const returns = inspected.definition.returns === void 0 ? void 0 : getValueDescriptor(inspected.definition.returns);
31569
+ const errors = inspected.definition.errors === void 0 ? void 0 : getValueDescriptor(inspected.definition.errors);
31570
+ if (inspected.definition.returns !== void 0 && returns === void 0 || inspected.definition.errors !== void 0 && errors === void 0) return yield* InvalidServerFunctionExport.make({
31571
+ exportName,
31572
+ message: `${moduleName}.${exportName} uses a validator that was not created by Ignotum values.`,
31573
+ path: modulePath
31574
+ });
31575
+ if (inspected.definition._tag === "Query" && args !== void 0 && descriptorContainsFile(args)) return yield* InvalidServerFunctionExport.make({
31576
+ exportName,
31577
+ message: `${moduleName}.${exportName} cannot accept files in query arguments.`,
31578
+ path: modulePath
31579
+ });
31580
+ if (inspected.definition._tag === "Mutation" && returns !== void 0 && descriptorContainsFile(returns)) return yield* InvalidServerFunctionExport.make({
31581
+ exportName,
31582
+ message: `${moduleName}.${exportName} cannot return files from a mutation.`,
31583
+ path: modulePath
31584
+ });
31585
+ if (errors !== void 0 && descriptorContainsFile(errors)) return yield* InvalidServerFunctionExport.make({
31586
+ exportName,
31587
+ message: `${moduleName}.${exportName} cannot include files in application errors.`,
31588
+ path: modulePath
31589
+ });
31590
+ const discovered = {
31254
31591
  address: FunctionAddress.make(`api.${moduleName}.${exportName}`),
31255
31592
  exportName,
31256
31593
  kind: inspected.definition._tag,
31257
31594
  moduleName,
31258
31595
  modulePath
31596
+ };
31597
+ const withArgs = args === void 0 ? discovered : {
31598
+ ...discovered,
31599
+ args
31600
+ };
31601
+ const withErrors = errors === void 0 ? withArgs : {
31602
+ ...withArgs,
31603
+ errors
31604
+ };
31605
+ functions.push(returns === void 0 ? withErrors : {
31606
+ ...withErrors,
31607
+ returns
31259
31608
  });
31260
31609
  }
31261
31610
  return Array$1.sortWith(functions, (definition) => definition.address, String$1.Order);
@@ -31339,6 +31688,7 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
31339
31688
  appDirectory,
31340
31689
  nodeModulesDirectory(),
31341
31690
  packageRoot,
31691
+ path.join(packageRoot, "node_modules"),
31342
31692
  ...workspaceProbes
31343
31693
  ].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
31344
31694
  const exitCode = yield* spawner.exitCode(ChildProcess.make(process$1.execPath, [
@@ -31445,12 +31795,24 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
31445
31795
  if (!(yield* fileSystem.exists(absoluteSourceMap))) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit a source map.` });
31446
31796
  const bundlePath = normalizePath(path.join("server", relativeFile));
31447
31797
  const sourceMapPath = normalizePath(path.join("server", relativeSourceMap));
31448
- return {
31798
+ const artifact = {
31449
31799
  address: definition.address,
31450
31800
  kind: definition.kind,
31451
31801
  bundle: yield* artifactReference(bundlePath, yield* fileSystem.readFile(absoluteFile)),
31452
31802
  sourceMap: yield* artifactReference(sourceMapPath, yield* fileSystem.readFile(absoluteSourceMap))
31453
31803
  };
31804
+ const withArgs = definition.args === void 0 ? artifact : {
31805
+ ...artifact,
31806
+ args: definition.args
31807
+ };
31808
+ const withErrors = definition.errors === void 0 ? withArgs : {
31809
+ ...withArgs,
31810
+ errors: definition.errors
31811
+ };
31812
+ return definition.returns === void 0 ? withErrors : {
31813
+ ...withErrors,
31814
+ returns: definition.returns
31815
+ };
31454
31816
  });
31455
31817
  const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, outputDirectory, functionModules) {
31456
31818
  const fileSystem = yield* FileSystem.FileSystem;
@@ -31463,7 +31825,7 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
31463
31825
  yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
31464
31826
  const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
31465
31827
  const manifest = {
31466
- formatVersion: 1,
31828
+ formatVersion: 2,
31467
31829
  schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
31468
31830
  functions
31469
31831
  };
@@ -31848,7 +32210,7 @@ const make$2 = (options) => Effect$1.gen(function* () {
31848
32210
  return transformRows ? Effect$1.map(effect, transformRows) : effect;
31849
32211
  },
31850
32212
  executeStream(_sql, _params) {
31851
- return Stream.die("executeStream not implemented");
32213
+ return Stream$1.die("executeStream not implemented");
31852
32214
  },
31853
32215
  backup(destination) {
31854
32216
  return Effect$1.suspend(() => {
@@ -31912,6 +32274,113 @@ const sqliteCauseWithErrno = (cause) => {
31912
32274
  return Object.assign(cause, { errno: errcode });
31913
32275
  };
31914
32276
  //#endregion
32277
+ //#region ../contracts/dist/runtime/index.js
32278
+ const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
32279
+ const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
32280
+ const IndexScalarValue = Schema.Union([
32281
+ Schema.Boolean,
32282
+ Schema.Finite,
32283
+ Schema.String
32284
+ ]);
32285
+ const scalarValueKind = (value) => Schema.is(Schema.Boolean)(value) ? "boolean" : Schema.is(Schema.Finite)(value) ? "number" : "string";
32286
+ const encodeString = (value) => {
32287
+ const bytes = [];
32288
+ for (let index = 0; index < value.length; index += 1) {
32289
+ const unit = value.charCodeAt(index);
32290
+ bytes.push(1, unit >>> 8, unit & 255);
32291
+ }
32292
+ bytes.push(0);
32293
+ return Array.from(bytes);
32294
+ };
32295
+ const encodeNumber = (input) => {
32296
+ const value = Object.is(input, -0) ? 0 : input;
32297
+ if (!Number.isFinite(value)) throw new Error("Index numbers must be finite.");
32298
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
32299
+ new DataView(bytes.buffer).setFloat64(0, value, false);
32300
+ if ((bytes[0] ?? 0) >= 128) for (let index = 0; index < bytes.length; index += 1) bytes[index] = 255 - bytes[index];
32301
+ else bytes[0] = (bytes[0] ?? 0) ^ 128;
32302
+ return Array.from(bytes);
32303
+ };
32304
+ const scalarKind = (descriptor) => {
32305
+ switch (descriptor.type) {
32306
+ case "boolean": return "boolean";
32307
+ case "date":
32308
+ case "integer":
32309
+ case "number": return "number";
32310
+ case "id":
32311
+ case "string": return "string";
32312
+ case "literal": return scalarValueKind(descriptor.value);
32313
+ case "literals": {
32314
+ const first = descriptor.values[0];
32315
+ if (first === void 0) throw new Error("Indexed literal sets cannot be empty.");
32316
+ const kind = scalarValueKind(first);
32317
+ if (descriptor.values.some((value) => scalarValueKind(value) !== kind)) throw new Error("Indexed literal sets cannot mix value types.");
32318
+ return kind;
32319
+ }
32320
+ default: throw new Error(`Values of type '${descriptor.type}' cannot be indexed.`);
32321
+ }
32322
+ };
32323
+ const encodeScalar = (descriptor, value) => {
32324
+ switch (scalarKind(descriptor)) {
32325
+ case "boolean":
32326
+ if (!Schema.is(Schema.Boolean)(value)) throw new Error("Expected an indexed boolean value.");
32327
+ return [value ? 1 : 0];
32328
+ case "number":
32329
+ if (!Schema.is(Schema.Finite)(value)) throw new Error("Expected an indexed number value.");
32330
+ return encodeNumber(value);
32331
+ case "string":
32332
+ if (!Schema.is(Schema.String)(value)) throw new Error("Expected an indexed string value.");
32333
+ return encodeString(value);
32334
+ }
32335
+ };
32336
+ const encodeBytes = (bytes) => EncodedIndexKey.make(bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""));
32337
+ const encodeIndexPrefix = (descriptors, values) => {
32338
+ if (descriptors.length !== values.length) throw new Error("Index descriptors and values must have the same length.");
32339
+ const bytes = descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index]));
32340
+ if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
32341
+ return encodeBytes(bytes);
32342
+ };
32343
+ const encodeIndexKey = (descriptors, values, createdAt, id) => {
32344
+ const bytes = [
32345
+ ...descriptors.flatMap((descriptor, index) => encodeScalar(descriptor, values[index])),
32346
+ ...encodeNumber(createdAt),
32347
+ ...encodeString(id)
32348
+ ];
32349
+ if (bytes.length > schemaIndexLimits.keyBytes) throw new Error(`Index keys cannot exceed ${schemaIndexLimits.keyBytes} bytes.`);
32350
+ return encodeBytes(bytes);
32351
+ };
32352
+ const successorIndexPrefix = (prefix) => {
32353
+ const bytes = Array.from({ length: prefix.length / 2 }, (_, index) => Number.parseInt(prefix.slice(index * 2, index * 2 + 2), 16));
32354
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
32355
+ const byte = bytes[index];
32356
+ if (byte === void 0 || byte === 255) continue;
32357
+ bytes[index] = byte + 1;
32358
+ return encodeBytes(bytes.slice(0, index + 1));
32359
+ }
32360
+ };
32361
+ const encodeIndexRange = (descriptors, equal, lower, upper) => {
32362
+ const equalDescriptors = descriptors.slice(0, equal.length);
32363
+ const prefix = encodeIndexPrefix(equalDescriptors, equal);
32364
+ const rangeDescriptor = descriptors[equal.length];
32365
+ if ((lower !== void 0 || upper !== void 0) && rangeDescriptor === void 0) throw new Error("An index range exceeded the index fields.");
32366
+ const encodedLower = lower === void 0 ? prefix.length === 0 ? void 0 : prefix : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [lower.value])}`);
32367
+ const encodedUpper = upper === void 0 ? prefix.length === 0 ? void 0 : successorIndexPrefix(prefix) : EncodedIndexKey.make(`${prefix}${encodeIndexPrefix([rangeDescriptor], [upper.value])}`);
32368
+ const finalLower = encodedLower !== void 0 && lower !== void 0 && !lower.inclusive ? successorIndexPrefix(encodedLower) : encodedLower;
32369
+ const finalUpper = encodedUpper !== void 0 && upper !== void 0 && upper.inclusive ? successorIndexPrefix(encodedUpper) : encodedUpper;
32370
+ if (finalLower === void 0 && finalUpper === void 0) return {};
32371
+ if (finalLower === void 0) return { upper: finalUpper };
32372
+ if (finalUpper === void 0) return { lower: finalLower };
32373
+ return {
32374
+ lower: finalLower,
32375
+ upper: finalUpper
32376
+ };
32377
+ };
32378
+ const indexIdentity = (tableId, name, fields) => IndexIdentity.make(encodeCanonicalJson([
32379
+ tableId,
32380
+ name,
32381
+ fields
32382
+ ]));
32383
+ //#endregion
31915
32384
  //#region ../contracts/dist/runtime/hosted.js
31916
32385
  const QueryKey = Schema.String.pipe(Schema.brand("ignotum/hosted/QueryKey"));
31917
32386
  const TableDependency = Schema.Struct({
@@ -31923,20 +32392,71 @@ const DocumentDependency = Schema.Struct({
31923
32392
  tableId: TableId,
31924
32393
  id: GeneratedId
31925
32394
  });
31926
- const DependencyKey = Schema.Union([TableDependency, DocumentDependency]);
31927
- const DependencySet = Schema.Array(DependencyKey);
31928
- const InvalidationSet = Schema.Array(DependencyKey);
32395
+ const IndexRangeDependency = Schema.Struct({
32396
+ type: Schema.Literal("IndexRange"),
32397
+ tableId: TableId,
32398
+ index: IndexIdentity,
32399
+ lower: Schema.optional(EncodedIndexKey),
32400
+ upper: Schema.optional(EncodedIndexKey)
32401
+ });
32402
+ const IndexPointInvalidation = Schema.Struct({
32403
+ type: Schema.Literal("IndexPoint"),
32404
+ tableId: TableId,
32405
+ index: IndexIdentity,
32406
+ key: EncodedIndexKey
32407
+ });
32408
+ const ReadDependency = Schema.Union([
32409
+ TableDependency,
32410
+ DocumentDependency,
32411
+ IndexRangeDependency
32412
+ ]);
32413
+ const WriteInvalidation = Schema.Union([
32414
+ TableDependency,
32415
+ DocumentDependency,
32416
+ IndexPointInvalidation
32417
+ ]);
32418
+ Schema.Union([ReadDependency, IndexPointInvalidation]);
32419
+ const DependencySet = Schema.Array(ReadDependency);
32420
+ const InvalidationSet = Schema.Array(WriteInvalidation);
31929
32421
  const RuntimeQueryResult = Schema.Struct({
31930
32422
  type: Schema.Literal("Query"),
31931
32423
  result: WireResult,
31932
32424
  dependencies: DependencySet,
31933
- observedRevision: AppStateRevision
32425
+ observedRevision: AppStateRevision,
32426
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
32427
+ });
32428
+ const FileReferenceAdded = Schema.Struct({
32429
+ type: Schema.Literal("AddReference"),
32430
+ file: RuntimeFileMetadata,
32431
+ tableId: TableId,
32432
+ documentId: GeneratedId
31934
32433
  });
32434
+ const FileReferenceRemoved = Schema.Struct({
32435
+ type: Schema.Literal("RemoveReference"),
32436
+ fileId: FileId,
32437
+ tableId: TableId,
32438
+ documentId: GeneratedId
32439
+ });
32440
+ const FileReferenceEffect = Schema.Union([FileReferenceAdded, FileReferenceRemoved]);
32441
+ const FileEffectBatch = Schema.Struct({
32442
+ id: InvocationId,
32443
+ committedRevision: AppStateRevision,
32444
+ effects: Schema.Array(FileReferenceEffect)
32445
+ });
32446
+ Schema.Struct({ appId: AppId });
32447
+ Schema.Struct({ batches: Schema.Array(FileEffectBatch) });
32448
+ Schema.Struct({
32449
+ appId: AppId,
32450
+ invocationId: InvocationId,
32451
+ acknowledgedAt: Schema.Int
32452
+ });
32453
+ Schema.Struct({ completed: Schema.Literal(true) });
31935
32454
  const RuntimeMutationSuccess = Schema.Struct({
31936
32455
  type: Schema.Literal("Mutation"),
31937
32456
  result: WireSuccess,
31938
32457
  invalidations: InvalidationSet,
31939
- committedRevision: AppStateRevision
32458
+ committedRevision: AppStateRevision,
32459
+ fileEffects: Schema.optional(FileEffectBatch)
31940
32460
  });
31941
32461
  const RuntimeMutationFailure = Schema.Struct({
31942
32462
  type: Schema.Literal("Mutation"),
@@ -31959,7 +32479,8 @@ const QueryInvocation = Schema.Struct({
31959
32479
  const MutationInvocation = Schema.Struct({
31960
32480
  ...InvocationBase,
31961
32481
  type: Schema.Literal("Mutation"),
31962
- invocationId: InvocationId
32482
+ invocationId: InvocationId,
32483
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
31963
32484
  });
31964
32485
  Schema.Union([QueryInvocation, MutationInvocation]);
31965
32486
  Schema.Struct({
@@ -31997,10 +32518,19 @@ Schema.Union([
31997
32518
  RuntimeInvocationRejected,
31998
32519
  RuntimeInvocationUnavailable
31999
32520
  ]);
32000
- const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : encodeCanonicalJson([
32521
+ const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
32001
32522
  dependency.type,
32002
32523
  dependency.tableId,
32003
32524
  dependency.id
32525
+ ]) : dependency.type === "IndexPoint" ? encodeCanonicalJson([
32526
+ dependency.type,
32527
+ dependency.index,
32528
+ dependency.key
32529
+ ]) : encodeCanonicalJson([
32530
+ dependency.type,
32531
+ dependency.index,
32532
+ dependency.lower ?? null,
32533
+ dependency.upper ?? null
32004
32534
  ]);
32005
32535
  const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make(encodeCanonicalJson([
32006
32536
  deploymentId,
@@ -32022,14 +32552,98 @@ const documentDependency = (tableId, id) => ({
32022
32552
  tableId,
32023
32553
  id
32024
32554
  });
32555
+ const indexRangeDependency = (tableId, index, lower, upper) => ({
32556
+ type: "IndexRange",
32557
+ tableId,
32558
+ index,
32559
+ lower,
32560
+ upper
32561
+ });
32562
+ const indexPointInvalidation = (tableId, index, key) => ({
32563
+ type: "IndexPoint",
32564
+ tableId,
32565
+ index,
32566
+ key
32567
+ });
32025
32568
  //#endregion
32026
32569
  //#region ../runtime/dist/functions.js
32027
32570
  var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
32028
32571
  //#endregion
32572
+ //#region ../contracts/dist/runtime/transport.js
32573
+ const runtimeInvocationPath = "/v1/invoke";
32574
+ const runtimeRevisionPath = "/v1/revision";
32575
+ const runtimeIndexPreparePath = "/v1/indexes/prepare";
32576
+ const runtimeIndexCommitPath = "/v1/indexes/commit";
32577
+ const runtimeFileEffectsPath = "/v1/files/effects";
32578
+ const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
32579
+ const ignotumPathPrefix$1 = "/_ignotum";
32580
+ const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
32581
+ const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
32582
+ const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
32583
+ const RuntimeRequestPath = Schema.Literals([
32584
+ runtimeInvocationPath,
32585
+ runtimeRevisionPath,
32586
+ runtimeIndexPreparePath,
32587
+ runtimeIndexCommitPath,
32588
+ runtimeFileEffectsPath,
32589
+ runtimeFileEffectsAcknowledgePath
32590
+ ]);
32591
+ const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
32592
+ const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
32593
+ Schema.Struct({
32594
+ appId: AppId,
32595
+ deploymentId: Schema.optional(DeploymentId),
32596
+ nonce: RuntimeRequestNonce,
32597
+ timestamp: RuntimeRequestTimestamp,
32598
+ signature: RuntimeRequestSignature
32599
+ });
32600
+ Schema.Struct({
32601
+ method: Schema.Literal("POST"),
32602
+ path: RuntimeRequestPath,
32603
+ timestamp: RuntimeRequestTimestamp,
32604
+ nonce: RuntimeRequestNonce,
32605
+ appId: AppId,
32606
+ deploymentId: Schema.optional(DeploymentId),
32607
+ bodySha256: Sha256
32608
+ });
32609
+ Schema.Struct({
32610
+ appId: AppId,
32611
+ deploymentId: DeploymentId
32612
+ });
32613
+ Schema.Struct({ completed: Schema.Literal(true) });
32614
+ const RuntimeErrorCode = Schema.Literals([
32615
+ "InvalidRequest",
32616
+ "InvalidSignature",
32617
+ "RequestExpired",
32618
+ "RequestReplay",
32619
+ "InvocationRejected",
32620
+ "RuntimeUnavailable"
32621
+ ]);
32622
+ Schema.Struct({
32623
+ code: RuntimeErrorCode,
32624
+ message: Schema.String,
32625
+ rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
32626
+ });
32627
+ //#endregion
32628
+ //#region ../shared/dist/http-body.js
32629
+ const joinChunks = (chunks) => {
32630
+ const byteLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
32631
+ const bytes = new Uint8Array(byteLength);
32632
+ let offset = 0;
32633
+ for (const chunk of chunks) {
32634
+ bytes.set(chunk, offset);
32635
+ offset += chunk.byteLength;
32636
+ }
32637
+ return bytes;
32638
+ };
32639
+ const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => Stream.mapError(stream, (error) => error).pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.runCollect, Effect.map(joinChunks)));
32640
+ //#endregion
32029
32641
  //#region ../runtime/dist/sync.js
32030
32642
  const makeDependencyIndex = () => {
32031
32643
  const dependenciesByQuery = MutableHashMap.empty();
32032
32644
  const queriesByDependency = MutableHashMap.empty();
32645
+ const rangesByQuery = MutableHashMap.empty();
32646
+ const queriesByIndex = MutableHashMap.empty();
32033
32647
  const remove = (queryId) => {
32034
32648
  const dependencies = Option.getOrUndefined(MutableHashMap.get(dependenciesByQuery, queryId));
32035
32649
  if (dependencies === void 0) return;
@@ -32039,12 +32653,23 @@ const makeDependencyIndex = () => {
32039
32653
  MutableHashSet.remove(queries, queryId);
32040
32654
  if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByDependency, key);
32041
32655
  }
32656
+ const ranges = Option.getOrElse(MutableHashMap.get(rangesByQuery, queryId), () => []);
32657
+ for (const range of ranges) {
32658
+ const queries = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, range.index));
32659
+ if (queries === void 0) continue;
32660
+ MutableHashSet.remove(queries, queryId);
32661
+ if (MutableHashSet.size(queries) === 0) MutableHashMap.remove(queriesByIndex, range.index);
32662
+ }
32663
+ MutableHashMap.remove(rangesByQuery, queryId);
32042
32664
  MutableHashMap.remove(dependenciesByQuery, queryId);
32043
32665
  };
32044
32666
  const record = (queryId, dependencies) => {
32045
32667
  remove(queryId);
32046
- const keys = MutableHashSet.fromIterable(dependencies.map(dependencyKey));
32668
+ const exact = dependencies.filter((dependency) => dependency.type !== "IndexRange");
32669
+ const ranges = dependencies.filter((dependency) => dependency.type === "IndexRange");
32670
+ const keys = MutableHashSet.fromIterable(exact.map(dependencyKey));
32047
32671
  MutableHashMap.set(dependenciesByQuery, queryId, keys);
32672
+ MutableHashMap.set(rangesByQuery, queryId, ranges);
32048
32673
  for (const key of keys) {
32049
32674
  const current = Option.getOrElse(MutableHashMap.get(queriesByDependency, key), () => {
32050
32675
  const created = MutableHashSet.empty();
@@ -32053,10 +32678,24 @@ const makeDependencyIndex = () => {
32053
32678
  });
32054
32679
  MutableHashSet.add(current, queryId);
32055
32680
  }
32681
+ for (const range of ranges) {
32682
+ const current = Option.getOrElse(MutableHashMap.get(queriesByIndex, range.index), () => {
32683
+ const created = MutableHashSet.empty();
32684
+ MutableHashMap.set(queriesByIndex, range.index, created);
32685
+ return created;
32686
+ });
32687
+ MutableHashSet.add(current, queryId);
32688
+ }
32056
32689
  };
32057
32690
  const affected = (invalidations) => {
32058
32691
  const queries = MutableHashSet.empty();
32059
32692
  for (const invalidation of invalidations) {
32693
+ if (invalidation.type === "IndexPoint") {
32694
+ const indexed = Option.getOrUndefined(MutableHashMap.get(queriesByIndex, invalidation.index));
32695
+ if (indexed === void 0) continue;
32696
+ for (const queryId of indexed) if (Option.getOrElse(MutableHashMap.get(rangesByQuery, queryId), () => []).some((range) => range.index === invalidation.index && (range.lower === void 0 || invalidation.key >= range.lower) && (range.upper === void 0 || invalidation.key < range.upper))) MutableHashSet.add(queries, queryId);
32697
+ continue;
32698
+ }
32060
32699
  const current = Option.getOrUndefined(MutableHashMap.get(queriesByDependency, dependencyKey(invalidation)));
32061
32700
  if (current !== void 0) for (const queryId of current) MutableHashSet.add(queries, queryId);
32062
32701
  }
@@ -32069,43 +32708,6 @@ const makeDependencyIndex = () => {
32069
32708
  };
32070
32709
  };
32071
32710
  var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
32072
- //#endregion
32073
- //#region ../contracts/dist/runtime/transport.js
32074
- const runtimeInvocationPath = "/v1/invoke";
32075
- const runtimeRevisionPath = "/v1/revision";
32076
- const appSyncPath = `/_ignotum/v1/sync`;
32077
- const RuntimeRequestPath = Schema.Literals([runtimeInvocationPath, runtimeRevisionPath]);
32078
- const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
32079
- const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
32080
- Schema.Struct({
32081
- appId: AppId,
32082
- deploymentId: Schema.optional(DeploymentId),
32083
- nonce: RuntimeRequestNonce,
32084
- timestamp: RuntimeRequestTimestamp,
32085
- signature: RuntimeRequestSignature
32086
- });
32087
- Schema.Struct({
32088
- method: Schema.Literal("POST"),
32089
- path: RuntimeRequestPath,
32090
- timestamp: RuntimeRequestTimestamp,
32091
- nonce: RuntimeRequestNonce,
32092
- appId: AppId,
32093
- deploymentId: Schema.optional(DeploymentId),
32094
- bodySha256: Sha256
32095
- });
32096
- const RuntimeErrorCode = Schema.Literals([
32097
- "InvalidRequest",
32098
- "InvalidSignature",
32099
- "RequestExpired",
32100
- "RequestReplay",
32101
- "InvocationRejected",
32102
- "RuntimeUnavailable"
32103
- ]);
32104
- Schema.Struct({
32105
- code: RuntimeErrorCode,
32106
- message: Schema.String,
32107
- rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
32108
- });
32109
32711
  const syncPath = appSyncPath;
32110
32712
  const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
32111
32713
  //#endregion
@@ -32153,7 +32755,50 @@ const initialDevelopmentSchema = Effect.gen(function* () {
32153
32755
  ) STRICT
32154
32756
  `;
32155
32757
  });
32156
- const developmentMigrationLoader = Migrator.fromRecord({ "0001_initial_development_schema": initialDevelopmentSchema });
32758
+ const addDevelopmentIndexes = Effect.gen(function* () {
32759
+ const sql = yield* Client.SqlClient;
32760
+ yield* sql`
32761
+ CREATE TABLE indexes (
32762
+ id INTEGER PRIMARY KEY NOT NULL,
32763
+ tableId TEXT NOT NULL REFERENCES tables(id) ON DELETE CASCADE,
32764
+ identity TEXT NOT NULL UNIQUE,
32765
+ name TEXT NOT NULL,
32766
+ fields TEXT NOT NULL CHECK (json_valid(fields))
32767
+ ) STRICT
32768
+ `;
32769
+ yield* sql`
32770
+ CREATE TABLE indexEntries (
32771
+ indexId INTEGER NOT NULL REFERENCES indexes(id) ON DELETE CASCADE,
32772
+ tableId TEXT NOT NULL,
32773
+ documentId TEXT NOT NULL,
32774
+ key TEXT NOT NULL,
32775
+ PRIMARY KEY (indexId, documentId),
32776
+ FOREIGN KEY (tableId, documentId)
32777
+ REFERENCES documents(tableId, id)
32778
+ ON DELETE CASCADE
32779
+ ) STRICT
32780
+ `;
32781
+ yield* sql`
32782
+ CREATE UNIQUE INDEX index_entries_by_key
32783
+ ON indexEntries (indexId, key)
32784
+ `;
32785
+ });
32786
+ const addApplicationFiles = Effect.gen(function* () {
32787
+ yield* (yield* Client.SqlClient)`
32788
+ CREATE TABLE applicationFiles (
32789
+ fileId TEXT PRIMARY KEY NOT NULL,
32790
+ name TEXT NOT NULL,
32791
+ format TEXT NOT NULL CHECK (format IN ('jpeg', 'png', 'webp', 'avif', 'gif')),
32792
+ size INTEGER NOT NULL CHECK (size >= 0),
32793
+ ready INTEGER NOT NULL DEFAULT 0 CHECK (ready IN (0, 1))
32794
+ ) STRICT
32795
+ `;
32796
+ });
32797
+ const developmentMigrationLoader = Migrator.fromRecord({
32798
+ "0001_initial_development_schema": initialDevelopmentSchema,
32799
+ "0002_application_indexes": addDevelopmentIndexes,
32800
+ "0003_application_files": addApplicationFiles
32801
+ });
32157
32802
  var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
32158
32803
  static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
32159
32804
  loader: developmentMigrationLoader,
@@ -32168,6 +32813,16 @@ const StoredDocument = Schema.Struct({
32168
32813
  updatedAt: Schema.Int,
32169
32814
  fields: Schema.String
32170
32815
  });
32816
+ const IndexedStoredDocument = Schema.Struct({
32817
+ ...StoredDocument.fields,
32818
+ key: Schema.String
32819
+ });
32820
+ const StoredIndex = Schema.Struct({
32821
+ id: Schema.Natural,
32822
+ identity: Schema.String,
32823
+ name: Schema.String,
32824
+ fields: Schema.String
32825
+ });
32171
32826
  const StoredTable = Schema.Struct({
32172
32827
  id: TableId,
32173
32828
  name: Schema.String
@@ -32240,6 +32895,18 @@ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function*
32240
32895
  };
32241
32896
  });
32242
32897
  const nextUpdatedAt = (now, previous) => Math.max(DateTime.toEpochMillis(now), previous + 1);
32898
+ const decodeIndexScalar = Schema.decodeUnknownSync(IndexScalarValue);
32899
+ const indexDescriptors = (table, fields) => fields.map((field) => {
32900
+ const schema = table.fields[field];
32901
+ const descriptor = schema === void 0 ? void 0 : getValueDescriptor(schema);
32902
+ if (descriptor === void 0) throw new Error(`Index field '${field}' has no descriptor.`);
32903
+ return descriptor;
32904
+ });
32905
+ const storedIndexKey = (index, row) => {
32906
+ const fields = Schema.decodeSync(Schema.fromJsonString(Schema.JsonObject))(row.fields);
32907
+ const values = index.definition.fields.map((field) => decodeIndexScalar(fields[field]));
32908
+ return encodeIndexKey(index.descriptors, values, row.createdAt, row.id);
32909
+ };
32243
32910
  var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-runtime/database/LocalDatabase") {
32244
32911
  static layer = Layer.effect(LocalDatabase, Effect.gen(function* () {
32245
32912
  const ids = yield* IdGenerator;
@@ -32283,6 +32950,31 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32283
32950
  FROM documents
32284
32951
  WHERE tableId = ${tableId}
32285
32952
  ORDER BY createdAt ASC, id ASC
32953
+ `
32954
+ });
32955
+ const listIndexes = SqlSchema.findAll({
32956
+ Request: TableLookup,
32957
+ Result: StoredIndex,
32958
+ execute: ({ tableId }) => sql`
32959
+ SELECT id, identity, name, fields
32960
+ FROM indexes
32961
+ WHERE tableId = ${tableId}
32962
+ ORDER BY id ASC
32963
+ `
32964
+ });
32965
+ const insertIndex = SqlSchema.findOne({
32966
+ Request: Schema.Struct({
32967
+ tableId: TableId,
32968
+ identity: Schema.String,
32969
+ name: Schema.String,
32970
+ fields: Schema.String
32971
+ }),
32972
+ Result: StoredIndex,
32973
+ execute: ({ tableId, identity, name, fields }) => sql`
32974
+ INSERT INTO indexes (tableId, identity, name, fields)
32975
+ VALUES (${tableId}, ${identity}, ${name}, ${fields})
32976
+ ON CONFLICT (identity) DO UPDATE SET identity = excluded.identity
32977
+ RETURNING id, identity, name, fields
32286
32978
  `
32287
32979
  });
32288
32980
  const currentRevision = SqlSchema.findOne({
@@ -32316,6 +33008,64 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32316
33008
  definition
32317
33009
  };
32318
33010
  });
33011
+ const resolvedIndexes = Effect.fn("LocalDatabase.resolvedIndexes")(function* (table) {
33012
+ const stored = yield* listIndexes({ tableId: table.id });
33013
+ return Object.values(table.definition.indexes).map((definition) => {
33014
+ const identity = indexIdentity(table.id, definition.name, definition.fields);
33015
+ const row = stored.find((candidate) => candidate.identity === identity);
33016
+ if (row === void 0) throw new Error(`Index '${definition.name}' is not prepared.`);
33017
+ return {
33018
+ ...row,
33019
+ definition,
33020
+ descriptors: indexDescriptors(table.definition, definition.fields)
33021
+ };
33022
+ });
33023
+ });
33024
+ const prepareIndexes = Effect.fn("LocalDatabase.prepareIndexes")(function* (schema) {
33025
+ for (const [tableName, definition] of Object.entries(schema[SchemaDefinitionTypeId])) {
33026
+ const table = yield* resolveTable(schema, tableName);
33027
+ const stored = yield* listIndexes({ tableId: table.id });
33028
+ const expected = /* @__PURE__ */ new Set();
33029
+ for (const index of Object.values(definition.indexes)) {
33030
+ const identity = indexIdentity(table.id, index.name, index.fields);
33031
+ expected.add(identity);
33032
+ if (stored.some((candidate) => candidate.identity === identity)) continue;
33033
+ const fields = JSON.stringify(index.fields);
33034
+ const resolved = {
33035
+ ...yield* insertIndex({
33036
+ tableId: table.id,
33037
+ identity,
33038
+ name: index.name,
33039
+ fields
33040
+ }).pipe(Effect.catchTags({ NoSuchElementError: Effect.die })),
33041
+ definition: index,
33042
+ descriptors: indexDescriptors(definition, index.fields)
33043
+ };
33044
+ const documents = yield* collectStored({ tableId: table.id });
33045
+ yield* Effect.forEach(documents, (document) => sql`
33046
+ INSERT INTO indexEntries (indexId, tableId, documentId, key)
33047
+ VALUES (${resolved.id}, ${table.id}, ${document.id}, ${storedIndexKey(resolved, document)})
33048
+ `);
33049
+ }
33050
+ yield* Effect.forEach(stored.filter((index) => !expected.has(index.identity)), (index) => sql`DELETE FROM indexes WHERE id = ${index.id}`);
33051
+ }
33052
+ });
33053
+ const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (table, row) {
33054
+ return (yield* resolvedIndexes(table)).map((index) => ({
33055
+ index,
33056
+ key: storedIndexKey(index, row),
33057
+ identity: indexIdentity(table.id, index.name, index.definition.fields)
33058
+ }));
33059
+ });
33060
+ const writeIndexEntries = Effect.fn("LocalDatabase.writeIndexEntries")(function* (table, row) {
33061
+ const keys = yield* indexKeys(table, row);
33062
+ yield* Effect.forEach(keys, ({ index, key }) => sql`
33063
+ INSERT INTO indexEntries (indexId, tableId, documentId, key)
33064
+ VALUES (${index.id}, ${table.id}, ${row.id}, ${key})
33065
+ ON CONFLICT (indexId, documentId) DO UPDATE SET key = excluded.key
33066
+ `);
33067
+ return keys;
33068
+ });
32319
33069
  const find = Effect.fn("LocalDatabase.find")(function* (schema, tableName, id, dependencies) {
32320
33070
  const table = yield* resolveTable(schema, tableName);
32321
33071
  const documentId = GeneratedId.make(id);
@@ -32327,11 +33077,100 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32327
33077
  if (Option.isNone(row)) return void 0;
32328
33078
  return yield* toRuntimeDocument(table.definition, tableName, row.value, "get");
32329
33079
  });
32330
- const collect = Effect.fn("LocalDatabase.collect")(function* (schema, tableName, dependencies) {
33080
+ const queryDocuments = Effect.fn("LocalDatabase.queryDocuments")(function* (schema, tableName, selectedIndex, bounds, order, limit, pagination, dependencies) {
32331
33081
  const table = yield* resolveTable(schema, tableName);
32332
- dependencies?.record(tableDependency(table.id));
32333
- const rows = yield* collectStored({ tableId: table.id });
32334
- return yield* Effect.forEach(rows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
33082
+ if (selectedIndex === void 0) {
33083
+ dependencies?.record(tableDependency(table.id));
33084
+ const queryIdentity = paginationQueryIdentity(table.id, null, void 0, void 0, order);
33085
+ const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
33086
+ if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
33087
+ const cursor = decodedCursor?.position;
33088
+ if (cursor !== void 0 && cursor.type !== "Table") throw new Error("A pagination cursor has the wrong position type.");
33089
+ const clauses = ["tableId = ?"];
33090
+ const parameters = [table.id];
33091
+ if (cursor !== void 0) {
33092
+ const operator = order === "Asc" ? ">" : "<";
33093
+ clauses.push(`(createdAt ${operator} ? OR (createdAt = ? AND id ${operator} ?))`);
33094
+ parameters.push(cursor.createdAt, cursor.createdAt, cursor.id);
33095
+ }
33096
+ const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
33097
+ if (storageLimit !== void 0) parameters.push(storageLimit);
33098
+ const rows = yield* sql.unsafe(`SELECT id, createdAt, updatedAt, fields
33099
+ FROM documents
33100
+ WHERE ${clauses.join(" AND ")}
33101
+ ORDER BY createdAt ${order === "Asc" ? "ASC" : "DESC"}, id ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
33102
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(StoredDocument))(rows);
33103
+ const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
33104
+ const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
33105
+ const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
33106
+ const last = pageRows.at(-1);
33107
+ return {
33108
+ documents,
33109
+ nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
33110
+ type: "Table",
33111
+ createdAt: last.createdAt,
33112
+ id: last.id
33113
+ })
33114
+ };
33115
+ }
33116
+ const index = (yield* resolvedIndexes(table)).find((candidate) => candidate.name === selectedIndex);
33117
+ if (index === void 0) throw new Error(`Unknown index '${selectedIndex}'.`);
33118
+ const identity = indexIdentity(table.id, index.name, index.definition.fields);
33119
+ const queryIdentity = paginationQueryIdentity(table.id, identity, bounds.lower, bounds.upper, order);
33120
+ const decodedCursor = pagination?.cursor === null || pagination === void 0 ? void 0 : decodePaginationCursor(pagination.cursor);
33121
+ if (decodedCursor !== void 0 && decodedCursor.query !== queryIdentity) throw new Error("A pagination cursor belongs to another query.");
33122
+ const cursor = decodedCursor?.position;
33123
+ if (cursor !== void 0 && cursor.type !== "Index") throw new Error("A pagination cursor has the wrong position type.");
33124
+ const cursorLower = cursor !== void 0 && order === "Asc" ? successorIndexPrefix(cursor.key) : void 0;
33125
+ const cursorUpper = cursor !== void 0 && order === "Desc" ? cursor.key : void 0;
33126
+ const effectiveBounds = {
33127
+ lower: bounds.lower === void 0 ? cursorLower : cursorLower === void 0 || bounds.lower > cursorLower ? bounds.lower : cursorLower,
33128
+ upper: bounds.upper === void 0 ? cursorUpper : cursorUpper === void 0 || bounds.upper < cursorUpper ? bounds.upper : cursorUpper
33129
+ };
33130
+ const clauses = ["entries.indexId = ?"];
33131
+ const parameters = [index.id];
33132
+ if (effectiveBounds.lower !== void 0) {
33133
+ clauses.push("entries.key >= ?");
33134
+ parameters.push(effectiveBounds.lower);
33135
+ }
33136
+ if (effectiveBounds.upper !== void 0) {
33137
+ clauses.push("entries.key < ?");
33138
+ parameters.push(effectiveBounds.upper);
33139
+ }
33140
+ const storageLimit = pagination === void 0 ? limit : pagination.pageSize + 1;
33141
+ if (storageLimit !== void 0) parameters.push(storageLimit);
33142
+ const rows = yield* sql.unsafe(`SELECT documents.id, documents.createdAt, documents.updatedAt, documents.fields,
33143
+ entries.key
33144
+ FROM indexEntries AS entries
33145
+ JOIN documents
33146
+ ON documents.tableId = entries.tableId
33147
+ AND documents.id = entries.documentId
33148
+ WHERE ${clauses.join(" AND ")}
33149
+ ORDER BY entries.key ${order === "Asc" ? "ASC" : "DESC"}${storageLimit === void 0 ? "" : " LIMIT ?"}`, parameters);
33150
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(IndexedStoredDocument))(rows);
33151
+ const hasMore = pagination !== void 0 && decoded.length > pagination.pageSize;
33152
+ const pageRows = pagination === void 0 ? decoded : decoded.slice(0, pagination.pageSize);
33153
+ let dependencyBounds = effectiveBounds;
33154
+ if (pagination === void 0 ? limit !== void 0 && limit > 0 && decoded.length === limit : hasMore) {
33155
+ const last = pageRows.at(-1);
33156
+ if (last !== void 0) dependencyBounds = order === "Asc" ? {
33157
+ ...effectiveBounds,
33158
+ upper: successorIndexPrefix(EncodedIndexKey.make(last.key))
33159
+ } : {
33160
+ ...effectiveBounds,
33161
+ lower: EncodedIndexKey.make(last.key)
33162
+ };
33163
+ }
33164
+ dependencies?.record(indexRangeDependency(table.id, identity, dependencyBounds.lower, dependencyBounds.upper));
33165
+ const documents = yield* Effect.forEach(pageRows, (row) => toRuntimeDocument(table.definition, tableName, row, "collect"));
33166
+ const last = pageRows.at(-1);
33167
+ return {
33168
+ documents,
33169
+ nextCursor: !hasMore || last === void 0 ? null : encodePaginationCursor(queryIdentity, {
33170
+ type: "Index",
33171
+ key: EncodedIndexKey.make(last.key)
33172
+ })
33173
+ };
32335
33174
  });
32336
33175
  const insert = Effect.fn("LocalDatabase.insert")(function* (schema, tableName, value, invalidations) {
32337
33176
  const table = yield* resolveTable(schema, tableName);
@@ -32342,71 +33181,196 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32342
33181
  INSERT INTO documents (id, tableId, createdAt, updatedAt, fields)
32343
33182
  VALUES (${id}, ${table.id}, ${now}, ${now}, ${fields})
32344
33183
  `;
32345
- invalidations.record(tableDependency(table.id), documentDependency(table.id, id));
33184
+ const keys = yield* writeIndexEntries(table, {
33185
+ id,
33186
+ createdAt: now,
33187
+ updatedAt: now,
33188
+ fields
33189
+ });
33190
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, id), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32346
33191
  return id;
32347
33192
  });
32348
33193
  const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id, invalidations) {
32349
- const table = yield* resolveTable(schema, tableName);
33194
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32350
33195
  const documentId = GeneratedId.make(id);
32351
- yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`;
32352
- invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
33196
+ const stored = yield* findStored({
33197
+ id,
33198
+ tableId: table.id
33199
+ }).pipe(Effect.orDie);
33200
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33201
+ const keys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33202
+ yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`.pipe(Effect.orDie);
33203
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32353
33204
  });
32354
33205
  const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value, invalidations) {
32355
- const table = yield* resolveTable(schema, tableName);
33206
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32356
33207
  const documentId = GeneratedId.make(id);
32357
33208
  const stored = yield* findStored({
32358
33209
  id,
32359
33210
  tableId: table.id
32360
- });
32361
- if (Option.isSome(stored)) {
32362
- const current = yield* decodeFields(table.definition, tableName, stored.value, "patch");
32363
- const fields = yield* encodeFields(table.definition, tableName, {
32364
- ...current,
32365
- ...value
32366
- }, "patch", id);
32367
- const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
32368
- yield* sql`
32369
- UPDATE documents
32370
- SET fields = ${fields}, updatedAt = ${updatedAt}
32371
- WHERE id = ${id} AND tableId = ${table.id}
32372
- `;
32373
- }
32374
- invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
33211
+ }).pipe(Effect.orDie);
33212
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33213
+ const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33214
+ const current = yield* decodeFields(table.definition, tableName, stored.value, "patch").pipe(Effect.orDie);
33215
+ const fields = yield* encodeFields(table.definition, tableName, {
33216
+ ...current,
33217
+ ...value
33218
+ }, "patch", id).pipe(Effect.orDie);
33219
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
33220
+ yield* sql`
33221
+ UPDATE documents
33222
+ SET fields = ${fields}, updatedAt = ${updatedAt}
33223
+ WHERE id = ${id} AND tableId = ${table.id}
33224
+ `.pipe(Effect.orDie);
33225
+ const newKeys = yield* writeIndexEntries(table, {
33226
+ ...stored.value,
33227
+ fields,
33228
+ updatedAt
33229
+ }).pipe(Effect.orDie);
33230
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32375
33231
  });
32376
33232
  const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value, invalidations) {
32377
- const table = yield* resolveTable(schema, tableName);
33233
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32378
33234
  const documentId = GeneratedId.make(id);
32379
33235
  const stored = yield* findStored({
32380
33236
  id,
32381
33237
  tableId: table.id
32382
- });
32383
- if (Option.isSome(stored)) {
32384
- const fields = yield* encodeFields(table.definition, tableName, value, "replace", id);
32385
- const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
32386
- yield* sql`
32387
- UPDATE documents
32388
- SET fields = ${fields}, updatedAt = ${updatedAt}
32389
- WHERE id = ${id} AND tableId = ${table.id}
32390
- `;
32391
- }
32392
- invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId));
32393
- });
32394
- const makeReader = (schema, dependencies) => Object.freeze({
32395
- find: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
32396
- get: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie, Effect.flatMap((document) => document === void 0 ? Effect.fail(documentNotFound(tableName, id)) : Effect.succeed(document)))),
32397
- query: (tableName) => Object.freeze({ collect: () => resultFromEffect(collect(schema, tableName, dependencies).pipe(Effect.orDie)) })
33238
+ }).pipe(Effect.orDie);
33239
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33240
+ const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33241
+ const fields = yield* encodeFields(table.definition, tableName, value, "replace", id).pipe(Effect.orDie);
33242
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
33243
+ yield* sql`
33244
+ UPDATE documents
33245
+ SET fields = ${fields}, updatedAt = ${updatedAt}
33246
+ WHERE id = ${id} AND tableId = ${table.id}
33247
+ `.pipe(Effect.orDie);
33248
+ const newKeys = yield* writeIndexEntries(table, {
33249
+ ...stored.value,
33250
+ fields,
33251
+ updatedAt
33252
+ }).pipe(Effect.orDie);
33253
+ invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32398
33254
  });
33255
+ const makeReader = (schema, dependencies) => {
33256
+ const databaseQuery = (tableName, selectedIndex, selectedBounds = {}, order = "Asc") => {
33257
+ const table = schema[SchemaDefinitionTypeId][tableName];
33258
+ if (table === void 0) throw new Error(`Unknown database table '${tableName}'.`);
33259
+ const run = (limit) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, limit, void 0, dependencies).pipe(Effect.map(({ documents }) => documents), Effect.orDie);
33260
+ const runPage = (pagination) => queryDocuments(schema, tableName, selectedIndex, selectedBounds, order, void 0, pagination, dependencies).pipe(Effect.orDie);
33261
+ const validateLimit = (count) => {
33262
+ if (!Number.isInteger(count) || count < 0 || count > 1e3) throw new Error("Query limits must be integers between 0 and 1000.");
33263
+ return count;
33264
+ };
33265
+ return Object.freeze({
33266
+ index: (name, buildRange) => {
33267
+ if (selectedIndex !== void 0) throw new Error("A query can select only one index.");
33268
+ const definition = table.indexes[name];
33269
+ if (definition === void 0) throw new Error(`Unknown index '${name}' on table '${tableName}'.`);
33270
+ if (buildRange === void 0) return databaseQuery(tableName, name, {}, order);
33271
+ const fields = [
33272
+ ...definition.fields,
33273
+ "createdAt",
33274
+ "id"
33275
+ ];
33276
+ const descriptors = [
33277
+ ...indexDescriptors(table, definition.fields),
33278
+ { type: "date" },
33279
+ {
33280
+ type: "id",
33281
+ table: tableName
33282
+ }
33283
+ ];
33284
+ const equal = [];
33285
+ let lower;
33286
+ let upper;
33287
+ let position = 0;
33288
+ const encodeValue = (field, value) => {
33289
+ const codec = field === "createdAt" ? Schema.DateFromMillis : field === "id" ? GeneratedId : table.fields[field];
33290
+ if (codec === void 0) throw new Error(`Unknown index field '${field}'.`);
33291
+ return decodeIndexScalar(Schema.encodeUnknownSync(codec)(value));
33292
+ };
33293
+ const expectField = (field) => {
33294
+ const expected = fields[position];
33295
+ if (field !== expected) throw new Error(`Expected index field '${expected}', received '${field}'.`);
33296
+ };
33297
+ const builder = {
33298
+ eq: (field, value) => {
33299
+ if (lower !== void 0 || upper !== void 0) throw new Error("Equality cannot follow an index bound.");
33300
+ expectField(field);
33301
+ equal.push(encodeValue(field, value));
33302
+ position += 1;
33303
+ return builder;
33304
+ },
33305
+ gt: (field, value) => {
33306
+ expectField(field);
33307
+ if (lower !== void 0) throw new Error("An index range has one lower bound.");
33308
+ lower = {
33309
+ value: encodeValue(field, value),
33310
+ inclusive: false
33311
+ };
33312
+ return builder;
33313
+ },
33314
+ gte: (field, value) => {
33315
+ expectField(field);
33316
+ if (lower !== void 0) throw new Error("An index range has one lower bound.");
33317
+ lower = {
33318
+ value: encodeValue(field, value),
33319
+ inclusive: true
33320
+ };
33321
+ return builder;
33322
+ },
33323
+ lt: (field, value) => {
33324
+ expectField(field);
33325
+ if (upper !== void 0) throw new Error("An index range has one upper bound.");
33326
+ upper = {
33327
+ value: encodeValue(field, value),
33328
+ inclusive: false
33329
+ };
33330
+ return builder;
33331
+ },
33332
+ lte: (field, value) => {
33333
+ expectField(field);
33334
+ if (upper !== void 0) throw new Error("An index range has one upper bound.");
33335
+ upper = {
33336
+ value: encodeValue(field, value),
33337
+ inclusive: true
33338
+ };
33339
+ return builder;
33340
+ }
33341
+ };
33342
+ buildRange(builder);
33343
+ return databaseQuery(tableName, name, encodeIndexRange(descriptors, equal, lower, upper), order);
33344
+ },
33345
+ order: (direction) => databaseQuery(tableName, selectedIndex, selectedBounds, direction === "asc" ? "Asc" : "Desc"),
33346
+ collect: () => resultFromEffect(run()),
33347
+ take: (count) => resultFromEffect(run(validateLimit(count))),
33348
+ first: () => resultFromEffect(run(1).pipe(Effect.map((documents) => documents[0]))),
33349
+ unique: () => resultFromEffect(run(2).pipe(Effect.flatMap((documents) => documents.length > 1 ? Effect.die(/* @__PURE__ */ new Error("A unique query matched more than one document.")) : Effect.succeed(documents[0])))),
33350
+ paginate: (options) => resultFromEffect(runPage(options).pipe(Effect.map(({ documents, nextCursor }) => ({
33351
+ items: documents,
33352
+ nextCursor
33353
+ }))))
33354
+ });
33355
+ };
33356
+ return Object.freeze({
33357
+ find: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie)),
33358
+ get: (tableName, id) => resultFromEffect(find(schema, tableName, id, dependencies).pipe(Effect.orDie, Effect.flatMap((document) => document === void 0 ? Effect.fail(documentNotFound(tableName, id)) : Effect.succeed(document)))),
33359
+ query: (tableName) => databaseQuery(tableName)
33360
+ });
33361
+ };
32399
33362
  const makeWriter = (schema, invalidations) => {
32400
33363
  const reader = makeReader(schema);
32401
33364
  return Object.freeze({
32402
33365
  ...reader,
32403
- delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations).pipe(Effect.orDie)),
33366
+ delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)),
32404
33367
  insert: (tableName, value) => resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
32405
- patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations).pipe(Effect.orDie)),
32406
- replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations).pipe(Effect.orDie))
33368
+ patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations)),
33369
+ replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations))
32407
33370
  });
32408
33371
  };
32409
33372
  const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
33373
+ yield* prepareIndexes(schema).pipe(Effect.orDie);
32410
33374
  const dependencies = makeDependencyRecorder();
32411
33375
  const value = yield* use(Object.freeze({ db: makeReader(schema, dependencies) }));
32412
33376
  const { revision: observedRevision } = yield* currentRevision(void 0).pipe(Effect.orDie);
@@ -32417,6 +33381,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32417
33381
  };
32418
33382
  }));
32419
33383
  const trackedMutationTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
33384
+ yield* prepareIndexes(schema).pipe(Effect.orDie);
32420
33385
  const invalidations = makeDependencyRecorder();
32421
33386
  const value = yield* use(Object.freeze({ db: makeWriter(schema, invalidations) }));
32422
33387
  const { revision: committedRevision } = yield* advanceRevision(void 0).pipe(Effect.orDie);
@@ -32512,12 +33477,19 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
32512
33477
  onFailure: (error) => encodeFailure(resolved.definition, error),
32513
33478
  onSuccess: (value) => encodeSuccess(resolved.definition, value)
32514
33479
  }));
32515
- return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) => ({
32516
- type: "Query",
32517
- result: value,
32518
- dependencies,
32519
- observedRevision
32520
- }))) : mutationSemaphore.withPermits(1)(database.trackedMutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.map(({ committedRevision, invalidations, value }) => ({
33480
+ return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) => {
33481
+ const files = value.type === "Success" && value.value !== void 0 ? encodedFileOccurrencesOf(value.value) : [];
33482
+ const result = {
33483
+ type: "Query",
33484
+ result: value,
33485
+ dependencies,
33486
+ observedRevision
33487
+ };
33488
+ return files.length === 0 ? result : {
33489
+ ...result,
33490
+ files
33491
+ };
33492
+ })) : mutationSemaphore.withPermits(1)(database.trackedMutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.map(({ committedRevision, invalidations, value }) => ({
32521
33493
  type: "Mutation",
32522
33494
  result: value,
32523
33495
  invalidations,
@@ -32566,6 +33538,252 @@ const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function*
32566
33538
  }) });
32567
33539
  }));
32568
33540
  //#endregion
33541
+ //#region ../contracts/dist/schema/file-content.js
33542
+ const ascii = (bytes) => {
33543
+ let value = "";
33544
+ for (let offset = 0; offset < bytes.length; offset += 16384) value += String.fromCharCode(...bytes.subarray(offset, offset + 16384));
33545
+ return value;
33546
+ };
33547
+ const startsWith = (bytes, signature) => signature.every((byte, index) => bytes[index] === byte);
33548
+ const endsWith = (bytes, signature) => signature.every((byte, index) => bytes[bytes.length - signature.length + index] === byte);
33549
+ const validJpeg = (bytes) => bytes.length >= 4 && startsWith(bytes, [
33550
+ 255,
33551
+ 216,
33552
+ 255
33553
+ ]) && endsWith(bytes, [255, 217]);
33554
+ const validPng = (bytes) => bytes.length >= 20 && startsWith(bytes, [
33555
+ 137,
33556
+ 80,
33557
+ 78,
33558
+ 71,
33559
+ 13,
33560
+ 10,
33561
+ 26,
33562
+ 10
33563
+ ]) && ascii(bytes.subarray(bytes.length - 12, bytes.length - 8)) === "IEND";
33564
+ const validGif = (bytes) => bytes.length >= 14 && (ascii(bytes.subarray(0, 6)) === "GIF87a" || ascii(bytes.subarray(0, 6)) === "GIF89a") && bytes.at(-1) === 59;
33565
+ const validWebp = (bytes) => {
33566
+ if (bytes.length < 12 || ascii(bytes.subarray(0, 4)) !== "RIFF" || ascii(bytes.subarray(8, 12)) !== "WEBP") return false;
33567
+ return ((bytes[4] ?? 0) | (bytes[5] ?? 0) << 8 | (bytes[6] ?? 0) << 16 | (bytes[7] ?? 0) << 24) + 8 === bytes.length;
33568
+ };
33569
+ const validAvif = (bytes) => {
33570
+ if (bytes.length < 16 || ascii(bytes.subarray(4, 8)) !== "ftyp") return false;
33571
+ const boxLength = (bytes[0] ?? 0) << 24 | (bytes[1] ?? 0) << 16 | (bytes[2] ?? 0) << 8 | (bytes[3] ?? 0);
33572
+ if (boxLength < 16 || boxLength > bytes.length) return false;
33573
+ for (let offset = 8; offset + 4 <= boxLength; offset += 4) {
33574
+ const brand = ascii(bytes.subarray(offset, offset + 4));
33575
+ if (brand === "avif" || brand === "avis") return true;
33576
+ }
33577
+ return false;
33578
+ };
33579
+ const validateApplicationFile = (bytes, format, declaredSize) => {
33580
+ if (bytes.byteLength !== declaredSize) return {
33581
+ valid: false,
33582
+ message: "The uploaded byte length does not match its preparation."
33583
+ };
33584
+ if (bytes.byteLength === 0 || bytes.byteLength > fileLimits.fileBytes) return {
33585
+ valid: false,
33586
+ message: `Application files must be between 1 and ${fileLimits.fileBytes} bytes.`
33587
+ };
33588
+ return (format === "jpeg" ? validJpeg(bytes) : format === "png" ? validPng(bytes) : format === "gif" ? validGif(bytes) : format === "webp" ? validWebp(bytes) : validAvif(bytes)) ? { valid: true } : {
33589
+ valid: false,
33590
+ message: `The upload is not a valid ${format} image.`
33591
+ };
33592
+ };
33593
+ //#endregion
33594
+ //#region src/dev-runtime/files.ts
33595
+ Schema.Struct({
33596
+ fileId: FileId,
33597
+ name: Schema.String,
33598
+ format: RuntimeFileOccurrence.fields.file.fields.format,
33599
+ size: Schema.Natural,
33600
+ ready: Schema.Literals([0, 1])
33601
+ });
33602
+ Schema.Struct({ fields: Schema.String });
33603
+ const JsonObjectString = Schema.fromJsonString(Schema.JsonObject);
33604
+ const LocalApplicationFiles = Context.Reference("ignotum/dev-runtime/files/LocalApplicationFiles", { defaultValue: () => ({
33605
+ admit: (_invocationId, _functionAddress, args) => Effect.succeed(encodedFileOccurrencesOf(args)),
33606
+ grants: (_subscriptionId, files) => Effect.succeed(files.map((file) => ({
33607
+ ...file,
33608
+ url: ""
33609
+ }))),
33610
+ prepare: (_invocationId, _functionAddress, _args, files) => Effect.succeed(files),
33611
+ upload: () => Effect.die("Local application file storage is unavailable."),
33612
+ releaseGrants: () => Effect.void,
33613
+ readGrant: () => Effect.succeed(Option.none()),
33614
+ reconcile: Effect.void,
33615
+ releasePreparation: () => Effect.void
33616
+ }) });
33617
+ const sameFile = (left, right) => left.id === right.id && left.format === right.format && left.name === right.name && left.size === right.size;
33618
+ const validFilename = (name) => {
33619
+ const bytes = new TextEncoder().encode(name).byteLength;
33620
+ const hasControlCharacter = Array.from(name).some((character) => {
33621
+ const codePoint = character.codePointAt(0) ?? 0;
33622
+ return codePoint <= 31 || codePoint === 127;
33623
+ });
33624
+ return bytes > 0 && bytes <= fileLimits.filenameBytes && !hasControlCharacter && !name.includes("/") && !name.includes("\\");
33625
+ };
33626
+ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicationFiles, Effect.gen(function* () {
33627
+ const ids = yield* IdGenerator;
33628
+ const fileSystem = yield* FileSystem.FileSystem;
33629
+ const path = yield* Path$1.Path;
33630
+ const sql = yield* SqlClient.SqlClient;
33631
+ const directory = path.join(appDirectory, ".ignotum", "files");
33632
+ yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie);
33633
+ const preparations = /* @__PURE__ */ new Map();
33634
+ const uploadFiles = /* @__PURE__ */ new Map();
33635
+ const grants = /* @__PURE__ */ new Map();
33636
+ const objectPath = (fileId) => path.join(directory, fileId);
33637
+ const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, files) {
33638
+ const existing = preparations.get(invocationId);
33639
+ if (existing !== void 0) {
33640
+ if (existing.function !== functionAddress || encodeCanonicalJson(existing.args) !== encodeCanonicalJson(args) || encodeCanonicalJson(existing.files) !== encodeCanonicalJson(files)) throw new Error("The invocation ID already belongs to another file preparation.");
33641
+ return files.map((occurrence) => {
33642
+ const token = existing.uploads.get(occurrence.file.id);
33643
+ return token === void 0 ? occurrence : {
33644
+ ...occurrence,
33645
+ url: `${fileUploadUrlPrefix}${token}`
33646
+ };
33647
+ });
33648
+ }
33649
+ const all = new Map(encodedFileOccurrencesOf(args).map((occurrence) => [encodeCanonicalJson(occurrence.path), occurrence.file]));
33650
+ const distinct = /* @__PURE__ */ new Map();
33651
+ for (const occurrence of files) {
33652
+ const arg = all.get(encodeCanonicalJson(occurrence.path));
33653
+ if (arg === void 0 || !sameFile(arg, occurrence.file) || !validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
33654
+ const prior = distinct.get(occurrence.file.id);
33655
+ if (prior !== void 0 && !sameFile(prior, occurrence.file)) throw new Error("One file ID has conflicting metadata.");
33656
+ distinct.set(occurrence.file.id, occurrence.file);
33657
+ }
33658
+ if (distinct.size > fileLimits.filesPerMutation) throw new Error("Too many files.");
33659
+ if ([...distinct.values()].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error("File uploads are too large.");
33660
+ const staged = yield* sql`
33661
+ SELECT COALESCE(SUM(size), 0) AS bytes FROM applicationFiles WHERE ready = 0
33662
+ `;
33663
+ let additionalStagedBytes = 0;
33664
+ for (const file of distinct.values()) {
33665
+ const stored = (yield* sql`
33666
+ SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
33667
+ `)[0];
33668
+ if (stored === void 0) additionalStagedBytes += file.size;
33669
+ else if (!sameFile({
33670
+ id: stored.fileId,
33671
+ name: stored.name,
33672
+ format: stored.format,
33673
+ size: stored.size
33674
+ }, file)) throw new Error(`File '${file.id}' has conflicting metadata.`);
33675
+ }
33676
+ if ((staged[0]?.bytes ?? 0) + additionalStagedBytes > fileLimits.stagedBytesPerApp) throw new Error("The app has too many staged file bytes.");
33677
+ const uploads = /* @__PURE__ */ new Map();
33678
+ for (const file of distinct.values()) {
33679
+ const stored = (yield* sql`
33680
+ SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
33681
+ `)[0];
33682
+ if (stored !== void 0 && stored.ready === 1) continue;
33683
+ const token = yield* ids.generate(FileUploadToken);
33684
+ uploads.set(file.id, token);
33685
+ uploadFiles.set(token, file);
33686
+ yield* sql`
33687
+ INSERT INTO applicationFiles (fileId, name, format, size, ready)
33688
+ VALUES (${file.id}, ${file.name}, ${file.format}, ${file.size}, 0)
33689
+ ON CONFLICT(fileId) DO UPDATE SET
33690
+ name = excluded.name, format = excluded.format, size = excluded.size
33691
+ `;
33692
+ }
33693
+ preparations.set(invocationId, {
33694
+ function: functionAddress,
33695
+ args,
33696
+ files,
33697
+ uploads
33698
+ });
33699
+ return files.map((occurrence) => {
33700
+ const token = uploads.get(occurrence.file.id);
33701
+ return token === void 0 ? occurrence : {
33702
+ ...occurrence,
33703
+ url: `${fileUploadUrlPrefix}${token}`
33704
+ };
33705
+ });
33706
+ });
33707
+ const admit = Effect.fn("LocalApplicationFiles.admit")(function* (invocationId, functionAddress, args) {
33708
+ const preparation = preparations.get(invocationId);
33709
+ if (preparation !== void 0 && (preparation.function !== functionAddress || encodeCanonicalJson(preparation.args) !== encodeCanonicalJson(args))) throw new Error("The mutation does not match its file preparation.");
33710
+ for (const occurrence of preparation?.files ?? []) if ((yield* sql`
33711
+ SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
33712
+ `)[0]?.ready !== 1) throw new Error("A prepared file has not finished uploading.");
33713
+ const occurrences = encodedFileOccurrencesOf(args);
33714
+ for (const occurrence of occurrences) if ((yield* sql`
33715
+ SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
33716
+ `)[0]?.ready !== 1) throw new Error(`File '${occurrence.file.id}' is unavailable.`);
33717
+ return occurrences;
33718
+ });
33719
+ const upload = Effect.fn("LocalApplicationFiles.upload")(function* (token, bytes) {
33720
+ const file = uploadFiles.get(token);
33721
+ if (file === void 0) throw new Error("The local file upload ticket is invalid.");
33722
+ const validation = validateApplicationFile(bytes, file.format, file.size);
33723
+ if (!validation.valid) throw new Error(validation.message);
33724
+ yield* fileSystem.writeFile(objectPath(file.id), bytes).pipe(Effect.orDie);
33725
+ yield* sql`UPDATE applicationFiles SET ready = 1 WHERE fileId = ${file.id}`;
33726
+ uploadFiles.delete(token);
33727
+ return "Stored";
33728
+ });
33729
+ const grantFiles = Effect.fn("LocalApplicationFiles.grants")(function* (subscriptionId, occurrences) {
33730
+ for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
33731
+ const tokens = /* @__PURE__ */ new Map();
33732
+ for (const occurrence of occurrences) {
33733
+ if (tokens.has(occurrence.file.id)) continue;
33734
+ const token = yield* ids.generate(FileGrantToken);
33735
+ tokens.set(occurrence.file.id, token);
33736
+ grants.set(token, {
33737
+ subscriptionId,
33738
+ file: occurrence.file
33739
+ });
33740
+ }
33741
+ return occurrences.map((occurrence) => ({
33742
+ ...occurrence,
33743
+ url: `${fileGrantUrlPrefix}${tokens.get(occurrence.file.id)}`
33744
+ }));
33745
+ });
33746
+ const reconcile = Effect.gen(function* () {
33747
+ const rows = yield* sql`
33748
+ SELECT fields FROM documents
33749
+ `;
33750
+ const referenced = /* @__PURE__ */ new Set();
33751
+ for (const row of rows) {
33752
+ const fields = yield* Schema.decodeEffect(JsonObjectString)(row.fields).pipe(Effect.orDie);
33753
+ for (const occurrence of encodedFileOccurrencesOf(fields)) referenced.add(occurrence.file.id);
33754
+ }
33755
+ const stored = yield* sql`
33756
+ SELECT fileId, name, format, size, ready FROM applicationFiles
33757
+ `;
33758
+ for (const file of stored) {
33759
+ if (referenced.has(file.fileId)) continue;
33760
+ yield* fileSystem.remove(objectPath(file.fileId), { force: true }).pipe(Effect.orDie);
33761
+ yield* sql`DELETE FROM applicationFiles WHERE fileId = ${file.fileId}`;
33762
+ }
33763
+ });
33764
+ return LocalApplicationFiles.of({
33765
+ admit: (...args) => admit(...args).pipe(Effect.orDie),
33766
+ grants: grantFiles,
33767
+ readGrant: (token) => {
33768
+ const grant = grants.get(token);
33769
+ return grant === void 0 ? Effect.succeed(Option.none()) : fileSystem.readFile(objectPath(grant.file.id)).pipe(Effect.orDie, Effect.map((bytes) => Option.some({
33770
+ file: grant.file,
33771
+ bytes
33772
+ })));
33773
+ },
33774
+ reconcile: reconcile.pipe(Effect.orDie),
33775
+ releaseGrants: (subscriptionId) => Effect.sync(() => {
33776
+ for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
33777
+ }),
33778
+ releasePreparation: (invocationId) => Effect.suspend(() => {
33779
+ if (!preparations.delete(invocationId)) return Effect.void;
33780
+ return reconcile.pipe(Effect.orDie);
33781
+ }),
33782
+ prepare: (...args) => prepare(...args).pipe(Effect.orDie),
33783
+ upload: (...args) => upload(...args).pipe(Effect.orDie)
33784
+ });
33785
+ }));
33786
+ //#endregion
32569
33787
  //#region src/dev-runtime/sync.ts
32570
33788
  const localSyncIdentity = {
32571
33789
  appId: AppId.make("app_000000000000000000000001"),
@@ -32643,6 +33861,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32643
33861
  const runtime = yield* FunctionRuntime;
32644
33862
  const invalidation = yield* QueryInvalidation;
32645
33863
  const mutationReplay = yield* MutationReplay;
33864
+ const files = yield* LocalApplicationFiles;
32646
33865
  const subscriptions = yield* Ref.make(HashMap.empty());
32647
33866
  const dependencyIndex = makeDependencyIndex();
32648
33867
  const invocationFibers = yield* FiberSet.make();
@@ -32681,11 +33900,16 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32681
33900
  if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
32682
33901
  dependencyIndex.record(subscriptionId, result.dependencies);
32683
33902
  if ((yield* invalidation.latestRevision) > result.observedRevision) continue;
32684
- yield* deliver({
33903
+ const granted = yield* files.grants(subscriptionId, result.files ?? []);
33904
+ const snapshot = {
32685
33905
  type: "Snapshot",
32686
33906
  id: subscriptionId,
32687
33907
  result: result.result,
32688
33908
  revision: result.observedRevision
33909
+ };
33910
+ yield* deliver(granted.length === 0 ? snapshot : {
33911
+ ...snapshot,
33912
+ files: granted
32689
33913
  });
32690
33914
  return;
32691
33915
  }
@@ -32713,6 +33937,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32713
33937
  yield* PubSub.take(invalidations).pipe(Effect.flatMap((event) => event.type === "All" ? refreshAll() : refreshAffected(event.invalidations)), Effect.forever, Effect.forkScoped);
32714
33938
  const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
32715
33939
  const operation = operationForSubscription(message.id);
33940
+ if (encodedFileOccurrencesOf(message.args).length > 0) {
33941
+ yield* send(protocolError("InvalidArguments", "Queries cannot take files as arguments.", operation));
33942
+ return;
33943
+ }
32716
33944
  const current = yield* Ref.get(subscriptions);
32717
33945
  if (HashMap.has(current, message.id)) {
32718
33946
  yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
@@ -32739,13 +33967,20 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32739
33967
  type: "Invocation",
32740
33968
  id: message.id
32741
33969
  };
33970
+ if (!(yield* files.admit(message.id, message.function, message.args).pipe(Effect.as(true), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be admitted: ${String(cause)}`, operation)).pipe(Effect.as(false)))))) {
33971
+ yield* files.releasePreparation(message.id);
33972
+ return;
33973
+ }
32742
33974
  const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
32743
33975
  FunctionUnavailable: (error) => sendResolutionError(operation, error),
32744
33976
  InvalidArguments: (error) => sendResolutionError(operation, error),
32745
33977
  UnknownFunction: (error) => sendResolutionError(operation, error),
32746
33978
  WrongFunctionKind: (error) => sendResolutionError(operation, error)
32747
33979
  }));
32748
- if (prepared === void 0) return;
33980
+ if (prepared === void 0) {
33981
+ yield* files.releasePreparation(message.id);
33982
+ return;
33983
+ }
32749
33984
  const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
32750
33985
  yield* mutationReplay.execute(message.id, input, prepared.execute.pipe(Effect.flatMap((result) => result.type === "Mutation" ? Effect.succeed(result) : Effect.die("Expected mutation")))).pipe(Effect.flatMap((result) => {
32751
33986
  if (!("committedRevision" in result)) return send({
@@ -32763,12 +33998,23 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32763
33998
  invalidations: result.invalidations,
32764
33999
  committedRevision: result.committedRevision
32765
34000
  })));
32766
- }), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
34001
+ }), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.ensuring(files.releasePreparation(message.id)), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
32767
34002
  cause,
32768
34003
  function: message.function,
32769
34004
  requestId: message.id
32770
34005
  }))), FiberSet.run(invocationFibers), Effect.asVoid);
32771
34006
  });
34007
+ const handlePrepareMutation = Effect.fn("SyncServer.handlePrepareMutation")(function* (message) {
34008
+ const operation = {
34009
+ type: "Invocation",
34010
+ id: message.id
34011
+ };
34012
+ yield* files.prepare(message.id, message.function, message.args, message.files).pipe(Effect.flatMap((uploads) => send({
34013
+ type: "MutationPrepared",
34014
+ id: message.id,
34015
+ uploads
34016
+ })), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34017
+ });
32772
34018
  const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
32773
34019
  const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
32774
34020
  if (message === void 0) return;
@@ -32779,6 +34025,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32779
34025
  case "Unsubscribe":
32780
34026
  dependencyIndex.remove(message.id);
32781
34027
  yield* Ref.update(subscriptions, HashMap.remove(message.id));
34028
+ yield* files.releaseGrants(message.id);
34029
+ return;
34030
+ case "PrepareMutation":
34031
+ yield* handlePrepareMutation(message);
32782
34032
  return;
32783
34033
  case "Invoke":
32784
34034
  yield* handleInvoke(message);
@@ -32788,7 +34038,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
32788
34038
  yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
32789
34039
  type: "Handshake",
32790
34040
  ...localSyncIdentity
32791
- }).pipe(Effect.orDie) });
34041
+ }).pipe(Effect.orDie) }).pipe(Effect.ensuring(Ref.get(subscriptions).pipe(Effect.flatMap((active) => Effect.forEach(HashMap.keys(active), (subscriptionId) => files.releaseGrants(subscriptionId))), Effect.asVoid)));
32792
34042
  });
32793
34043
  var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
32794
34044
  const makeHandlersLayer = (server, appDirectory, databasePath) => {
@@ -32796,7 +34046,7 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
32796
34046
  const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(idGeneratorLayer, sqliteLayer)));
32797
34047
  const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(idGeneratorLayer));
32798
34048
  const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(layer$1)), executorLayer)));
32799
- const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1);
34049
+ const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1, localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(idGeneratorLayer, sqliteLayer, layer$1))));
32800
34050
  return Layer.effect(SyncHandlers, Effect.gen(function* () {
32801
34051
  const invalidation = yield* QueryInvalidation;
32802
34052
  const fileSystem = yield* FileSystem.FileSystem;
@@ -32812,6 +34062,30 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
32812
34062
  const httpApp = Effect.gen(function* () {
32813
34063
  const request = yield* HttpServerRequest$1.HttpServerRequest;
32814
34064
  const pathname = new URL(request.url, "http://ignotum.local").pathname;
34065
+ if (pathname.startsWith(fileUploadUrlPrefix)) {
34066
+ if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
34067
+ const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
34068
+ if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
34069
+ const bytes = yield* collectStreamBytes(request.stream, fileLimits.fileBytes, () => /* @__PURE__ */ new Error("The local application file is too large.")).pipe(Effect.orDie);
34070
+ return yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.upload(token.value, bytes)), Effect.match({
34071
+ onFailure: (error) => HttpServerResponse.text(String(error), { status: 422 }),
34072
+ onSuccess: () => HttpServerResponse.text("Stored", { status: 201 })
34073
+ }));
34074
+ }
34075
+ if (pathname.startsWith(fileGrantUrlPrefix)) {
34076
+ if (request.method !== "GET" && request.method !== "HEAD") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
34077
+ const token = Schema.decodeOption(FileGrantToken)(pathname.slice(fileGrantUrlPrefix.length));
34078
+ if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
34079
+ const granted = yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.readGrant(token.value)));
34080
+ if (Option.isNone(granted)) return HttpServerResponse.text("Not Found", { status: 404 });
34081
+ const headers = {
34082
+ "cache-control": "private, no-store",
34083
+ "content-length": String(granted.value.bytes.byteLength),
34084
+ "content-type": fileMimeTypes[granted.value.file.format],
34085
+ "x-content-type-options": "nosniff"
34086
+ };
34087
+ return request.method === "HEAD" ? HttpServerResponse.empty({ headers }) : HttpServerResponse.uint8Array(granted.value.bytes, { headers });
34088
+ }
32815
34089
  return yield* HttpServerResponse.json(pathname === syncPath ? {
32816
34090
  code: "UpgradeRequired",
32817
34091
  message: `Connect to ${syncPath} with WebSocket.`
@@ -32991,28 +34265,58 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
32991
34265
  return yield* Effect.never;
32992
34266
  }));
32993
34267
  });
34268
+ //#endregion
34269
+ //#region ../../docs/agent/AGENTS.md?raw
34270
+ var AGENTS_default = "# Ignotum app instructions\n\nThis repository is an Ignotum app. Read `.agents/skills/ignotum/SKILL.md` before changing the\nschema, server functions, generated references, client code, app structure, or development setup.\n\n## Source boundaries\n\n- `server/schema.ts` defines tables and indexes.\n- TypeScript files directly inside `server` define public queries and mutations.\n- `client/index.tsx` default-exports the app definition and root component.\n- `shared` contains code that is safe in both client and server builds.\n- `_generated` is compiler output. Never edit it.\n\nUse `ignotum/server` only for schema authoring. Use generated builders from\n`@/_generated/server.js` in function modules. Use `ignotum/client` and\n`@/_generated/api.js` in client code. Keep the `.js` suffix on generated and aliased imports.\n\nDo not add an HTML entry, framework configuration, Tailwind configuration, API route, direct\ndatabase client, or backend server. Those do not belong to an Ignotum app.\n\n## Before finishing\n\nRun the app's existing checks. When no project-specific command says otherwise, regenerate the\nbindings and typecheck:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nDo not reset the development database unless the user explicitly asks to discard local data.\n";
34271
+ //#endregion
34272
+ //#region ../../docs/agent/skills/ignotum/SKILL.md?raw
34273
+ var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing a deployed app's fields, check\n[Deployment model](references/deployment-model.md). After the first\nactivation, hosted deployment requires the same table names, field names, and field validators.\nIndex-only changes are allowed. Do not promise that adding an optional field is compatible.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
34274
+ //#endregion
34275
+ //#region src/cli/agent-files.ts
34276
+ const references = [
34277
+ ["api.md", "---\ntitle: HTTP API\ndescription: Use the authenticated API at api.ignotum.cloud.\nsection: Reference\nslug: api\norder: 73\n---\n\n# HTTP API\n\nThe public API at `https://api.ignotum.cloud` creates apps and manages immutable deployments. The\n`ignotum deploy` command is the normal client for these endpoints. Use the HTTP API directly when\nbuilding deployment tooling or checking an app after an interrupted command.\n\nThe API does not expose application queries and mutations over HTTP. Deployed clients call those\nfunctions through the generated client API.\n\n## Authentication\n\nEvery route except `GET /health` requires a bearer token:\n\n```http\nAuthorization: Bearer <IGNOTUM_API_TOKEN>\n```\n\nTreat the token as an account credential. Do not include it in client code or send it from a\ndeployed app. The API does not enable browser CORS, so call it from a CLI, CI job, or other trusted\nserver environment.\n\nJSON requests require `Content-Type: application/json`. A JSON body may be at most 1 MiB. Responses\nuse JSON unless a route is not found.\n\n## Common values\n\nIgnotum IDs are opaque strings. Their prefixes identify the kind:\n\n```text\napp_000000000000000000000001\ndep_000000000000000000000001\nteam_000000000000000000000001\n```\n\nEach prefix is followed by 24 lowercase letters or digits. Do not parse or generate these IDs.\n\nTimestamps such as `createdAt` and `readyAt` are Unix time in milliseconds.\n\nAn app object has this shape:\n\n```json\n{\n \"id\": \"app_000000000000000000000001\",\n \"teamId\": \"team_000000000000000000000001\",\n \"slug\": \"team-todos\",\n \"activeDeploymentId\": \"dep_000000000000000000000001\",\n \"deploymentGeneration\": 3,\n \"createdAt\": 1787925600000\n}\n```\n\n`activeDeploymentId` is `null` before the first activation. `deploymentGeneration` starts at 0 and\nincreases when a deployment becomes active.\n\n## Health\n\n```http\nGET /health\n```\n\nA healthy API returns `200`:\n\n```json\n{ \"service\": \"api\", \"status\": \"ok\" }\n```\n\nAn infrastructure check failure returns `503` with `status` set to `unavailable`. This route does\nnot require authentication.\n\n## Create an app\n\n```http\nPOST /v1/apps\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"slug\": \"team-todos\" }\n```\n\nA slug is 1 through 63 lowercase letters, digits, or hyphens. It must start and end with a letter\nor digit. A successful request returns `201` and the new app object.\n\nThis operation is not idempotent. A repeated request for the same slug returns `SlugUnavailable`.\n\n## Get an app\n\nLook up an app by ID:\n\n```http\nGET /v1/apps/app_000000000000000000000001\nAuthorization: Bearer <token>\n```\n\nOr by slug:\n\n```http\nGET /v1/apps/by-slug/team-todos\nAuthorization: Bearer <token>\n```\n\nBoth routes return `200` and the same app shape. The authenticated account must own the app. The\nAPI currently has no route to list apps, rename a slug, or delete an app.\n\n## Deployment flow\n\nA deployment uses four steps:\n\n1. Begin with an inventory.\n2. Upload the canonical `inventory.json` and every file listed by it.\n3. Finalize the deployment.\n4. Activate the ready deployment.\n\nThe deployment is inactive until the last step succeeds.\n\n### Inventory\n\nThe begin request contains a version 1 inventory:\n\n```json\n{\n \"formatVersion\": 1,\n \"files\": [\n {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"kind\": \"ClientShell\",\n \"contentType\": \"text/html; charset=utf-8\"\n }\n ]\n}\n```\n\nEach file entry has:\n\n| Field | Meaning |\n| ----------------- | ---------------------------------------------------------------------- |\n| `path` | Relative artifact path with `/` separators and no `.` or `..` segment. |\n| `size` | Byte length as a non-negative integer. |\n| `sha256` | Lowercase 64-character SHA-256 digest. |\n| `kind` | The artifact role listed below. |\n| `contentType` | MIME type stored with the file. |\n| `contentEncoding` | Optional encoding metadata. |\n\nThe defined kinds are `ClientAsset`, `ClientDocument`, `ClientManifest`, `ClientPublicFile`,\n`ClientShell`, `FunctionBundle`, `ServerManifest`, `SourceMap`, and `SchemaSnapshot`.\n\nFiles must be sorted by `path`. The inventory must contain `client/manifest.json`,\n`client/shell.html`, `server/manifest.json`, and `server/schema.json`. Paths and kinds must agree,\nand every manifest reference must match the inventory. `inventory.json` is not listed because it\ncannot contain its own hash.\n\nThe byte limits are in [Limits](limits.md). The public `ignotum` package does not expose an artifact\nbuilder or a build-only command. The formats below let deployment tooling inspect and transport an\nartifact emitted by the CLI. Building function bundles with another compiler is not a supported\npublic extension point.\n\nCanonical `inventory.json` is UTF-8 JSON with object keys sorted lexicographically, array order\npreserved, no insignificant whitespace, and one trailing line feed. Its upload headers contain the\nbyte length and SHA-256 digest of those exact bytes.\n\n### Client manifest\n\n`client/manifest.json` connects hosted URL paths to client files:\n\n```json\n{\n \"formatVersion\": 1,\n \"shell\": {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"routes\": [\n {\n \"pathname\": \"/manual.pdf\",\n \"artifact\": {\n \"path\": \"client/routes/manual.pdf\",\n \"size\": 4096,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n }\n }\n ]\n}\n```\n\nThe shell reference must point to `client/shell.html`. Routes must be sorted by `pathname`. A\npathname starts with `/`, uses only letters, digits, `.`, `_`, `~`, `-`, and `/`, and cannot claim\n`/_ignotum`. Each route points to a matching `ClientDocument` or `ClientPublicFile` inventory entry.\nAn artifact can have only one route, and every document or public-file entry must have one.\n\nFiles under `client/assets/` use `ClientAsset`. Files under `client/routes/` use `ClientDocument`\nwhen their name ends in `.html`; AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files use\n`ClientPublicFile`. The four fixed client paths use their corresponding fixed kinds.\n\n### Server manifest\n\n`server/manifest.json` lists every callable function and the schema snapshot:\n\n```json\n{\n \"formatVersion\": 1,\n \"schema\": {\n \"path\": \"server/schema.json\",\n \"size\": 512,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"functions\": [\n {\n \"address\": \"api.todos.list\",\n \"kind\": \"Query\",\n \"bundle\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs\",\n \"size\": 2048,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n },\n \"sourceMap\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs.map\",\n \"size\": 1024,\n \"sha256\": \"123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0\"\n }\n }\n ]\n}\n```\n\nFunction addresses have the form `api.<module>.<export>`, where both names are TypeScript\nidentifiers. `kind` is `Query` or `Mutation`. Addresses and referenced paths must be unique. A\nbundle ends in `.mjs`; its source map is the same path followed by `.map`. Every `server/` file is\nthe manifest, schema, or one referenced bundle or source map.\n\n### Schema snapshot\n\n`server/schema.json` is version 2:\n\n```json\n{\n \"formatVersion\": 2,\n \"tables\": [\n {\n \"name\": \"todos\",\n \"fields\": [\n { \"name\": \"completed\", \"value\": { \"type\": \"boolean\" } },\n { \"name\": \"text\", \"value\": { \"type\": \"string\" } }\n ],\n \"indexes\": [{ \"name\": \"by_completed\", \"fields\": [\"completed\"] }]\n }\n ]\n}\n```\n\nSort tables and fields by name. Sort indexes by name and preserve each index's declared field\norder. A value descriptor uses one of these recursive forms:\n\n```text\n{ \"type\": \"boolean\" | \"date\" | \"integer\" | \"never\" | \"null\" | \"number\" | \"string\" }\n{ \"type\": \"id\", \"table\": string }\n{ \"type\": \"literal\", \"value\": string | finite number | boolean }\n{ \"type\": \"literals\", \"values\": Array<string | finite number | boolean> }\n{ \"type\": \"array\" | \"nullable\" | \"optional\" | \"record\", \"value\": descriptor }\n{ \"type\": \"union\", \"members\": Array<descriptor> }\n{ \"type\": \"object\", \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n{ \"type\": \"error\", \"tag\": string, \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n```\n\nSort object and error fields by name. This snapshot must describe the same schema used to build the\nfunction bundles. After the first activation, table and field descriptors must match the active\nsnapshot exactly; only the `indexes` arrays may change.\n\n### Begin\n\n```http\nPOST /v1/apps/{appId}/deployments\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"inventory\": { \"formatVersion\": 1, \"files\": [] } }\n```\n\nThe abbreviated body above shows the envelope only. Send a complete valid inventory. Success\nreturns `201` and a deployment object:\n\n```json\n{\n \"id\": \"dep_000000000000000000000002\",\n \"appId\": \"app_000000000000000000000001\",\n \"status\": \"Uploading\",\n \"inventory\": { \"formatVersion\": 1, \"files\": [] },\n \"createdAt\": 1787925601000,\n \"readyAt\": null\n}\n```\n\nThe response repeats the complete accepted inventory. This operation is not idempotent. Repeating\nit creates another deployment.\n\n### Upload a file\n\nUpload each path as one percent-encoded route component. For example,\n`client/shell.html` becomes `client%2Fshell.html`:\n\n```http\nPUT /v1/apps/{appId}/deployments/{deploymentId}/files/client%2Fshell.html\nAuthorization: Bearer <token>\nContent-Length: 128\nX-Ignotum-Sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n<128 raw bytes>\n```\n\n`Content-Length` and `X-Ignotum-Sha256` must match the accepted inventory entry. The body is the raw\nfile, not JSON. Upload the canonical `inventory.json` through the same route even though it is not\nin the `files` array.\n\nA new file returns:\n\n```json\n{ \"status\": \"Stored\" }\n```\n\nUploading the same path, bytes, and metadata again is safe and returns `AlreadyStored`. Different\ncontent at an existing path returns `DeploymentFileConflict`; deployment files are immutable.\n\n### Finalize\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/finalize\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Finalization checks that every expected file exists, no extra file exists,\nall metadata matches, and the client and server manifests are valid. Success returns `200` with the\ndeployment status changed to `Ready` and `readyAt` set.\n\nFinalizing an already ready deployment is safe and returns its current record.\n\n### Activate\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/activate\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Only a ready, unexpired deployment can be activated. Ignotum rejects a\nstored-schema change but permits index changes. See\n[Deployment model](deployment-model.md#schema-compatibility).\n\nSuccess returns `200` with the updated app object. Activating its current deployment again is safe.\nActivation reloads connected app clients.\n\n## Errors\n\nEvery recognized API failure has this body:\n\n```json\n{\n \"code\": \"SlugUnavailable\",\n \"message\": \"The app slug is unavailable.\"\n}\n```\n\nTreat `code` as the machine-readable value. Messages may change.\n\n| Status | Codes |\n| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `400` | `InvalidRequest`, `DeploymentFileUnexpected` |\n| `401` | `Unauthorized` |\n| `403` | `AppAccessDenied` |\n| `404` | `AppNotFound`, `DeploymentNotFound` |\n| `409` | `SlugUnavailable`, `DeploymentNotUploading`, `DeploymentFileConflict`, `DeploymentIncomplete`, `DeploymentNotReady`, `SchemaIncompatible`, `ActivationConflict` |\n| `503` | `ApiUnavailable` |\n\n`ApiUnavailable` does not prove whether a state-changing request reached its final step. Read the\napp before deciding how to recover from an unconfirmed activation. Do not automatically retry\n`POST /v1/apps` or the begin-deployment request because either may create a second resource.\n"],
34278
+ ["app-entry.md", "---\ntitle: App entry\ndescription: Define the Web title and root component in client/index.tsx.\nsection: Web\nslug: app-entry\norder: 41\n---\n\n# App entry\n\nThe required `client/index.tsx` file names the page and supplies its root component:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nKeep the entry in this form:\n\n- Import `app` by name from `ignotum/client`, without an alias.\n- Default-export one direct `app(...)` call.\n- Pass one object with exactly `title` and `component`.\n- Write both keys as unquoted properties, without a spread.\n- Use a non-empty quoted string literal for `title`.\n\nThe component can import and render other client modules.\n\nIgnotum does not use an `App.tsx` entry convention. The entry is `client/index.tsx`, and its default\nexport is the `app(...)` definition.\n\nNext: [Preact](preact.md)\n"],
34279
+ ["cli.md", "---\ntitle: CLI\ndescription: Look up Ignotum commands, flags, and environment variables.\nsection: Reference\nslug: cli\norder: 72\n---\n\n# CLI\n\nRun commands through the local or downloaded `ignotum` package:\n\n```sh\nnpx ignotum <command>\n```\n\n## Commands\n\n| Command | Purpose |\n| ---------------------- | ------------------------------------------------------------------ |\n| `ignotum new <path>` | Create an app in an empty directory. |\n| `ignotum install` | Install dependencies with pnpm, or npm if pnpm is absent. |\n| `ignotum codegen` | Update files under `_generated`. |\n| `ignotum dev` | Run the local development server. |\n| `ignotum dev db reset` | Delete the local development database while the server is stopped. |\n| `ignotum deploy` | Build, upload, and activate the app. |\n\nUse `ignotum <command> --help` for the command's generated help.\n\n## `new`\n\n```sh\nnpx ignotum new my-app\nnpx ignotum new . --no-git --no-install\n```\n\nThe target directory must be empty. The command installs dependencies and initializes a Git\nrepository by default. Use `--no-install` or `--no-git` to skip either action.\n\n## `dev`\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------- |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. |\n| `--open` | off | Open the app after the server starts. |\n\n## `deploy`\n\nUse `--app <slug>` to create or link the hosted app on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nLater deployments read the link from `.ignotum/app.json` and do not need the flag.\n\n## Environment variables\n\n| Variable | Meaning |\n| ------------------- | ------------------------------------------------------ |\n| `IGNOTUM_API_TOKEN` | Account token required by `deploy`. |\n| `IGNOTUM_API_URL` | API base URL. Defaults to `https://api.ignotum.cloud`. |\n\n`IGNOTUM_API_URL` must use HTTPS. A numeric loopback address such as `127.0.0.1` or `[::1]` may\nuse HTTP for local platform development.\n"],
34280
+ ["database.md", "---\ntitle: Database overview\nlabel: Overview\ndescription: Understand the persistent data model in an Ignotum app.\nsection: Database\nslug: database\norder: 10\n---\n\n# Database overview\n\nEach Ignotum app has a database described by `server/schema.ts`. A schema contains tables, stored\nfields, and indexes. Every stored document also has a table-specific ID and creation and update\ntimestamps.\n\nQueries and mutations read through `ctx.db`. Only mutations can insert, patch, replace, or delete\ndocuments. All reads and writes in one mutation commit together.\n\n- [Schema](schema.md) defines tables, fields, IDs, and schema changes.\n- [Values](values.md) lists the validators used for stored fields and function contracts.\n- [Reading data](reading-data.md) covers document and table reads.\n- [Writing data](writing-data.md) covers database changes and transaction behavior.\n- [Indexes](indexes.md) covers index definitions, ranges, and ordering.\n- [Pagination](pagination.md) covers cursor-based database and Web pagination.\n\nNext: [Schema](schema.md)\n"],
34281
+ ["deploy-app.md", "---\ntitle: Deploy an app\ndescription: Link an app and publish a deployment to Ignotum Cloud.\nsection: Deployments\nslug: deploy\norder: 51\n---\n\n# Deploy an app\n\nSet the API token you were issued:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nKeep the token outside the app directory and source control. It can create deployments for every\napp available to its account.\n\n## First deployment\n\nChoose the app slug on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nSlugs use lowercase letters, numbers, and hyphens. They are 1 through 63 characters, cannot start\nor end with a hyphen, and must be globally available. The deployed app uses\n`https://my-app.ignotum.app`.\n\nIf the slug already belongs to your account, the CLI links the local directory to that app. If it\nis available, the CLI creates the app. If you omit `--app`, it asks for a slug in an interactive\nterminal.\n\nThe CLI writes the app ID, slug, and API URL to `.ignotum/app.json`. It does not write the token\nthere. The generated `.gitignore` excludes `.ignotum`.\n\n## Later deployments\n\nOnce linked, run:\n\n```sh\nnpx ignotum deploy\n```\n\nThe command updates generated code, builds and validates the complete app, uploads a new\ndeployment, and activates it. There is no separate public build command.\n\nThe active deployment stays unchanged if any of these steps fail. See\n[Deployment model](deployment-model.md) for activation, schema compatibility, and interrupted\ndeployments.\n\nNext: [Deployment model](deployment-model.md)\n"],
34282
+ ["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command. To return to earlier behavior, deploy that source again as a new\ndeployment.\n\n## Schema compatibility\n\nThe first active deployment fixes the stored table and field schema. Later deployments may change\nWeb code, functions, and indexes. Changing a table name, adding or removing a stored field, or\nchanging a field validator causes activation to fail with `SchemaIncompatible`. The active\ndeployment and stored data remain unchanged.\n\nAdding, removing, renaming, or reordering indexes is supported. Ignotum prepares new indexes before\nthe new deployment starts accepting mutations.\n\n## Failed and interrupted deployments\n\nA build, upload, validation, compatibility, or activation failure leaves the current deployment\nactive.\n\nIf an upload or finalization request loses its connection, the CLI prints the new deployment ID.\nUnfinished uploads expire after 24 hours. If activation cannot be confirmed, use the\n[HTTP API](api.md) to inspect the app's `activeDeploymentId`, or run `ignotum deploy` again to create\nand activate a new deployment.\n\nSee [Guarantees](guarantees.md) for the activation contract and [Limits](limits.md) for deployment\nretention.\n"],
34283
+ ["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema, and\npublic assets. Ignotum activates these parts together after the complete deployment passes its\nchecks.\n\nUse `ignotum deploy` to link a local project to a hosted app and publish it. A failed build, upload,\nvalidation, or activation does not replace the active deployment.\n\n- [Deploy an app](deploy-app.md) covers the command, first-time linking, and later deployments.\n- [Deployment model](deployment-model.md) covers activation, schema compatibility, and interrupted\n deployments.\n- [Limits](limits.md) lists deployment size and retention limits.\n\nNext: [Deploy an app](deploy-app.md)\n"],
34284
+ ["errors.md", "---\ntitle: Errors\ndescription: Define, return, and handle typed application errors.\nsection: Functions\nslug: errors\norder: 24\n---\n\n# Errors\n\nFunctions can return typed application errors to the client. Ignotum keeps runtime failures and\nthrown exceptions separate from those public errors.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nIn a mutation, an application error rolls back every write from that invocation. In a query, it\nbecomes the settled failure for the subscription at the revision the query observed.\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n\nAn internal failure also rolls back a mutation. It does not become one of the application's\ndeclared errors, even if a thrown value happens to have the same shape.\n\n## Web matching\n\nThe client `Result.match` separates `error` and `internalError`. `error` handles only errors declared\nby the function. `internalError` handles the reserved platform error. If `internalError` is absent,\nmatching that result throws the error to the nearest UI error boundary.\n\nProtocol and connection failures are outside the application `Result`. A mutation promise rejects\nfor an operation-scoped protocol failure. A query hook throws a subscription infrastructure\nfailure during rendering so the UI error boundary can handle it.\n"],
34285
+ ["files.md", "---\ntitle: Files\ndescription: Validate, store, return, and use application files.\nsection: Files\nslug: files\norder: 30\n---\n\n# Files\n\nApplication files are database values. A Web form can pass a browser `File` to a mutation. The\nmutation stores its validated file value in a document, and a query can return it to Web code.\n\n## Declare a file value\n\nUse `values.file` in `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n profiles: table({\n name: values.string(),\n avatar: values.optional(values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 })),\n }),\n}));\n```\n\n`formats` must be a non-empty subset of `jpeg`, `png`, `webp`, `avif`, and `gif`. `maxBytes` is\noptional and cannot exceed the 10 MiB per-file limit. PDF, audio, video, SVG, archives, code, and\nexecutables are not accepted as application files.\n\n## Accept and store a file\n\nA mutation can accept the same file validator and store the result:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const setAvatar = mutation({\n args: {\n profileId: values.id(\"profiles\"),\n avatar: values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 }),\n },\n\n handler: function* (ctx, args) {\n yield* ctx.db.patch(\"profiles\", args.profileId, { avatar: args.avatar });\n },\n});\n```\n\nThe handler receives `name`, `format`, `mimeType`, and `size`, not the file bytes. Hosted functions\ncannot open or inspect file contents. Ignotum validates the file before invoking the mutation.\n\nMutation returns and application errors cannot contain files. Query arguments cannot contain\nfiles.\n\nIn Web code, pass a native `File` to the generated mutation caller:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction AvatarForm({ profileId }: { profileId: Id<\"profiles\"> }) {\n const setAvatar = useMutation(api.profiles.setAvatar);\n\n const change = (event: Event) => {\n const input = event.currentTarget as HTMLInputElement;\n const avatar = input.files?.[0];\n if (avatar === undefined) return;\n\n void setAvatar({ profileId, avatar }).then(\n Result.match({\n value: () => console.log(\"Saved\"),\n error: () => console.log(\"Could not save\"),\n }),\n );\n };\n\n return <input type=\"file\" accept=\"image/jpeg,image/png,image/webp\" onChange={change} />;\n}\n```\n\nThe browser's `File.type` must identify an allowed format. Ignotum also verifies the file bytes.\n\n## Return and use a file\n\nQueries can return file fields, including nested files and complete documents:\n\n```ts\nexport const get = query({\n args: { id: values.id(\"profiles\") },\n returns: values.doc(\"profiles\"),\n\n handler: function* (ctx, args) {\n return yield* ctx.db.get(\"profiles\", args.id);\n },\n});\n```\n\nUse `Files.url` on the current query value:\n\n```tsx\nimport { Files, Result, useQuery } from \"ignotum/client\";\n\nconst profile = useQuery(api.profiles.get, { id: profileId });\n\nreturn Result.match(profile, {\n pending: () => <span>Loading...</span>,\n value: (value) =>\n value.avatar === undefined ? (\n <span>No avatar</span>\n ) : (\n <img src={Files.url(value.avatar)} alt=\"\" />\n ),\n error: () => <span>Unavailable</span>,\n});\n```\n\nThe returned same-origin URL works in images, links, CSS URLs, and `fetch`. It is not a permanent\nURL. It remains valid while the query subscription and its current result authorize that file.\nUnsubscribing, disconnecting, replacing the result, making the query stale, or deleting the final\ndatabase reference revokes access. Do not save the URL as application data.\n\nCalling `Files.url` on a native file or a value that did not come from an active query throws.\n\n## Copy and remove references\n\nA file value returned by a query can be passed to another compatible mutation argument:\n\n```tsx\nconst copyAvatar = useMutation(api.profiles.copyAvatar);\nvoid copyAvatar({ targetId, avatar: source.avatar });\n```\n\nThe destination validator must allow the file's format and size. Removing one field or document\nremoves that reference. Deleting the final database reference deletes the application file. A\nstale Web value cannot restore it afterward.\n\nDocument changes and file references commit together. A failed mutation does not leave references\npartially changed.\n\n## Limits\n\n- One file is limited to 10 MiB.\n- One mutation may add up to 8 new files and 25 MiB of file data.\n- One app may have up to 100 MiB of uncommitted file data.\n- One query result may contain up to 256 distinct files.\n\nApplication files differ from files in `public`. Public assets belong to a deployment and have\nstable paths. See [Public assets](public-assets.md).\n\nNext: [Deployments](deployments.md)\n"],
34286
+ ["functions.md", "---\ntitle: Functions overview\nlabel: Overview\ndescription: Define callable application logic and use its generated API.\nsection: Functions\nslug: functions\norder: 21\n---\n\n# Functions overview\n\nQueries and mutations are callable application functions. Define them in `.ts` files directly\ninside `server`. The file name becomes the generated API module, and each exported function keeps\nits export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nFunction module names must be valid TypeScript identifiers. Ignotum ignores `schema.ts`,\n`index.ts`, files ending in `.test.ts` or `.spec.ts`, and names beginning with `_`. Nested files do\nnot become API modules.\n\n## Definitions\n\nImport schema-bound builders from the generated server file:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix. The generated definitions provide types and runtime validation for\narguments, successful returns, and application errors.\n\nThe `args` object describes one object parameter. Omit it when the function takes no arguments.\nOmit `returns` only when the handler returns `void`. Ignotum does not infer a runtime validator\nfrom a TypeScript return type.\n\nHandlers are generator functions. Use `yield*` for database and `Result` operations, then use an\nordinary `return` for a successful value. Do not mark a handler `async`.\n\n## Generated API\n\nCode generation writes browser references such as `api.todos.list` to `_generated/api.ts`. Web\ncode passes a reference to `useQuery`, `usePaginatedQuery`, or `useMutation`. The reference carries\nthe function's arguments, result, and errors into the call's TypeScript type.\n\n[Queries](queries.md) and [Mutations](mutations.md) cover each complete workflow.\n\n## Runtime boundary\n\nFunctions can import `_generated/server.ts`, `_generated/types.ts`, other server modules, shared\nmodules, and supported dependencies. They cannot import client files, `_generated/api.ts`,\n`ignotum/client`, Node built-ins, native add-ons, or dynamic imports.\n\nHosted functions have no direct network, filesystem, environment, or process access. Use the APIs\nprovided through the function context.\n\nSee [Values](values.md) for validators, [Errors](errors.md) for the typed error model, and\n[Limits](limits.md) for execution and data limits.\n\nNext: [Queries](queries.md)\n"],
34287
+ ["generated-code.md", "---\ntitle: Generated code\ndescription: Understand generated function references, builders, and data types.\nsection: Development\nslug: generated-code\norder: 61\n---\n\n# Generated code\n\nIgnotum generates the bindings that connect the schema, functions, and Web code. The development\nserver updates them before startup and when the relevant source changes.\n\nRun generation directly before a typecheck or an editor session without the development server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nCode generation writes three files:\n\n| File | Contents |\n| ---------------------- | ------------------------------------------------------- |\n| `_generated/server.ts` | Schema-bound `query`, `mutation`, and `values` exports. |\n| `_generated/api.ts` | Web references such as `api.todos.list`. |\n| `_generated/types.ts` | `DataModel`, `Doc`, and `Id` types. |\n\nDo not edit these files. Ignotum refuses to overwrite a conflicting file that lacks its generated\nheader. Move the conflicting file elsewhere and run `codegen` again.\n\nKeep the `.js` suffix when importing a generated module:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Doc, Id } from \"@/_generated/types.js\";\n```\n\nThe API file changes when function modules or exports change. Server builders and data types change\nwith the schema. See [Functions](functions.md) for function module naming rules.\n"],
34288
+ ["guarantees.md", "---\ntitle: Guarantees\ndescription: Understand consistency, atomicity, ordering, retries, and realtime updates.\nsection: Reference\nslug: guarantees\norder: 70\n---\n\n# Guarantees\n\nThis page states the behavior application code can rely on. It also marks the boundaries where\nIgnotum deliberately makes no promise.\n\n## Queries read one committed state\n\nA completed query result corresponds to one committed app-state revision. It does not combine\nreads from different successful mutations.\n\nQueries may run while mutations are committing. If app data changes during a hosted query,\nIgnotum discards that attempt and runs it again. The hosted runtime makes at most four consistency\nattempts within 15 seconds. If changes keep preventing a stable result, the query fails instead of\nreturning a mixed result.\n\nQueries have no write access. A query result can be a success, a declared application error, or an\ninternal failure. All three are tied to the revision observed by that execution.\n\n## Mutations are atomic\n\nA mutation either commits all of its writes or none of them.\n\nIgnotum commits only after the handler returns a value that matches its return validator. These\noutcomes commit no writes:\n\n- a declared application error;\n- an uncaught `DocumentNotFound` or other application error;\n- a thrown exception;\n- an invalid return value or error value;\n- an execution, data, or storage limit failure;\n- an internal platform failure before commit.\n\nDocument changes, index changes, the mutation result used for retries, and the new app-state\nrevision become durable together. A caller that receives a successful mutation result can rely on\nthe writes having committed.\n\n## Mutations are serialized per app\n\nOnly one mutation changes a given app at a time. A mutation sees every mutation that committed\nbefore it began, and it sees its own earlier writes. No other mutation can interleave between its\nreads and writes.\n\nDifferent apps do not share this serialization. Queries may also execute while a mutation is\nrunning, subject to the consistent-query rule above.\n\nDo not infer an ordering between mutation calls started concurrently. If one business action must\nhappen after another, await the first mutation result before starting the second, or combine the\nsteps in one mutation.\n\n## Reconnect retries do not repeat a retained mutation\n\nEach browser mutation call has one invocation ID. The client retains the call while a connection\nis unavailable and sends the same ID, function, and arguments after a reconnect to the same\ndeployment.\n\nIgnotum records the first completed result for that invocation ID. A retry with the same input\nreturns that result without running the handler or applying its writes again. This applies to\nsuccessful mutations and declared application errors.\n\nA reused ID with different input is rejected. An invocation's input includes the deployment,\nfunction, operation kind, and arguments.\n\nHosted retry records last for seven days and are capped at the newest 10,000 results per app. The\ndeduplication guarantee ends when a record expires or is evicted. Use an application-level unique\nkey when duplicate prevention must last longer.\n\nThe client does not replay pending mutations after it detects a different deployment. It reloads\nthe page instead, because running an old call against new server code would change its meaning.\n\n## Realtime queries follow committed writes\n\nWhile a query remains subscribed and the connection can make progress, Ignotum refreshes it after\na successful mutation that may affect one of its reads. Failed mutations cause no refresh because\nthey changed no data.\n\nDependency tracking follows the database operation:\n\n| Read | Writes that may refresh it |\n| ---------------- | ----------------------------------------------------------------------- |\n| `find` or `get` | A change to that document. |\n| Full table query | A change in that table. |\n| Indexed query | A change whose old or new index position intersects the relevant range. |\n\nBounded indexed reads track the part of the range that can affect their result. Ignotum may refresh\na query whose returned value ends up unchanged. It favors an unnecessary rerun over missing a\nrelevant write.\n\nThe mutation promise does not wait for every affected query to render its next value. The commit\nis complete when the promise resolves, while query refresh and UI rendering continue separately.\n\nAfter a temporary disconnect, the browser keeps the latest settled query result. It recreates the\nsubscription after reconnecting. Ignotum checks recovered hosted query state against current app\nstate before serving it, so a commit that happened during recovery cannot make an older cached\nresult look current.\n\nThere is no delivery-time guarantee. A broken connection or unavailable runtime can delay an\nupdate, and infrastructure failures may reach the UI error boundary. Realtime queries are current\nresults, not an event log. Intermediate values may be coalesced when several mutations happen\nbefore a refresh completes.\n\n## Results do not move backward\n\nEach query snapshot carries the revision it observed. The client ignores a snapshot older than the\nlatest revision already accepted for that subscription. A slow earlier refresh cannot replace a\nnewer result.\n\nThe revision is an ordering value for complete query results. It is not exposed as an application\nevent number, and the public client API does not provide a change stream or resume token.\n\n## Pagination is not a frozen snapshot\n\nOne page is consistent in the same way as any other query result. Several pages loaded at different\ntimes do not share one historical snapshot.\n\nThe cursor records the query and the last returned position. It prevents using a cursor with a\ndifferent table, index, range, or direction. It does not freeze rows or index positions. If data is\ninserted, deleted, or reordered between page loads, an item can move across the cursor boundary.\n\nThe client subscribes to every loaded page, so earlier pages can also change while the list is\nopen. Render with document IDs and treat the combined list as a live ordered view.\n\n## Deployment activation is all or nothing\n\nA deployment contains its client and server code together. Ignotum changes the active deployment\nonly after the complete upload passes validation and any new indexes are ready. A build, upload,\nvalidation, or compatibility failure leaves the current deployment active.\n\nActivation reloads connected clients. A session never intentionally combines the client from one\ndeployment with functions from another.\n\n## What is not guaranteed\n\nIgnotum does not promise:\n\n- an order for mutation calls started concurrently;\n- permanent mutation deduplication beyond the published retention limits;\n- delivery of every intermediate query value;\n- a time bound for realtime updates or reconnects to succeed;\n- snapshot isolation across several pagination calls;\n- uniqueness for an application index;\n- uninterrupted hosted availability during a platform or persistent-data outage.\n\n[Limits](limits.md) lists the bounds that qualify these guarantees.\n"],
34289
+ ["how-ignotum-works.md", "---\ntitle: How Ignotum works\ndescription: Understand how data, functions, files, Web code, and deployments fit together.\nsection: Start\nslug: how-ignotum-works\norder: 3\n---\n\n# How Ignotum works\n\nAn Ignotum app is one project with a database, functions, application files, and a browser-facing\nWeb part. A deployment publishes all of them together.\n\n## Database and functions\n\nThe schema names the app's tables and describes every stored field. Queries read the database.\nMutations read and write it in atomic operations.\n\nEach exported query or mutation receives a generated reference. Web code uses that reference\ninstead of a handwritten route or request type. The function's argument, result, and application\nerror definitions determine the corresponding TypeScript types in the browser.\n\n## Realtime results\n\nCalling a query from Web code creates a subscription. Ignotum records the database reads made by\nthe query and refreshes it after a committed mutation may have changed those reads. The UI receives\nthe current complete result rather than a stream of database operations.\n\nSeveral components using the same query and arguments share one browser subscription. A temporary\ndisconnect keeps the latest settled result while the client reconnects and recreates active\nsubscriptions.\n\n## Files\n\nApplication files are values that can appear in mutation arguments and database documents. Web\ncode passes a browser `File` to a mutation. The mutation stores the validated file value in a\ndocument, and a query can return it to Web code with a URL for the current query result.\n\nFiles in the top-level `public` directory are different. They are fixed deployment assets with\nstable paths and do not belong to database documents.\n\n## Web\n\nThe Web part starts at `client/index.tsx`. Its default `app(...)` export supplies the page title and\nroot Preact component. It calls functions through generated references and renders their typed\nresults.\n\n## Deployment\n\n`ignotum deploy` publishes the Web code, functions, schema, and public assets as one deployment.\nActivation changes them together. Existing browser sessions reload when they detect the new\ndeployment.\n\nNext: [Project structure](project-structure.md)\n"],
34290
+ ["index.md", "---\ntitle: Ignotum\nlabel: Introduction\ndescription: Learn the application model used by Ignotum.\nsection: Start\nslug: /\norder: 1\n---\n\n# Ignotum\n\n> Ignotum is currently in private alpha. APIs and hosted service behavior may change before public\n> availability.\n\nIgnotum is an application cloud for TypeScript apps. An app defines its data, callable functions,\nbrowser UI, and files in one project. The CLI runs the project locally and deploys it as one unit.\n\nThe database stores application data. Queries read that data, mutations change it, and generated\nreferences let the Web part call both with matching TypeScript types. Active queries receive new\nresults after relevant mutations commit.\n\nStart with the [Quickstart](quickstart.md). It creates a small app and follows the path from schema\nto deployment. [How Ignotum works](how-ignotum-works.md) explains how the parts fit together, and\n[Project structure](project-structure.md) explains the `client`, `server`, and `shared` directories.\n\nNext: [Quickstart](quickstart.md)\n"],
34291
+ ["indexes.md", "---\ntitle: Indexes\ndescription: Define indexes and use them for ranges and ordering.\nsection: Database\nslug: indexes\norder: 15\n---\n\n# Indexes\n\nAn index gives documents an order that functions can select and bound. Declare each index with its\ntable in `server/schema.ts`:\n\n```ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]),\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters. `by_channel_priority` supports a range for one channel, or for one channel and its\npriorities. It is not a priority-only index.\n\nAn index name must be non-empty, cannot begin with `$`, and is limited to 64 UTF-8 bytes. A table\ncannot repeat a name, repeat a field inside one index, or declare two indexes with the same fields\nin the same order. See [Limits](limits.md) for index counts and key size.\n\n## Select an index\n\nUse the declared name in a function:\n\n```ts\nconst todos =\n yield *\n ctx.db\n .query(\"todos\")\n .index(\"by_channel_priority\", (range) =>\n range.eq(\"channel\", args.channel).gte(\"priority\", args.minimumPriority),\n )\n .collect();\n```\n\nThe range builder follows the declared field order. Match any number of leading fields with `eq`,\nthen optionally bound the next field:\n\n| Method | Bound |\n| ------------------- | ------------------------------------------------- |\n| `eq(field, value)` | Equal to `value`. May continue to the next field. |\n| `gt(field, value)` | Greater than `value`. |\n| `gte(field, value)` | Greater than or equal to `value`. |\n| `lt(field, value)` | Less than `value`. |\n| `lte(field, value)` | Less than or equal to `value`. |\n\nChain one lower and one upper bound on the same field for a two-sided range:\n\n```ts\nconst thisWeek =\n yield *\n ctx.db\n .query(\"events\")\n .index(\"by_workspace_start\", (range) =>\n range\n .eq(\"workspaceId\", args.workspaceId)\n .gte(\"start\", args.weekStart)\n .lt(\"start\", args.nextWeek),\n )\n .collect();\n```\n\nCalling `index` without a range reads the whole index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders equal declared values by `createdAt`, then `id`. After matching all declared\nfields with `eq`, those fields are also available to the range builder.\n\n`order(\"desc\")` reverses the complete index order. It cannot sort by a field outside the selected\nindex.\n\nUse [Pagination](pagination.md) for large or incremental index results.\n"],
34292
+ ["limits.md", "---\ntitle: Limits\ndescription: Understand hosted limits for functions, data, connections, and deployments.\nsection: Reference\nslug: limits\norder: 71\n---\n\n# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports sync admission, execution time, result size, and app storage quota failures as\n`ResourceLimitExceeded`. Some lower-level document, collection, memory, stack, or operation limit\nfailures become `InternalServerError` instead. A deployment that exceeds a limit fails before\nactivation, so the current deployment stays active.\n\n## Functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries a query when app data changes while it runs. One logical execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nThe inventory contains at most 512 listed files. Its own `inventory.json` upload is additional.\nRequests with JSON bodies to `api.ignotum.cloud` are also limited to 1 MiB.\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
34293
+ ["local-development.md", "---\ntitle: Local development\ndescription: Run an Ignotum app locally and manage its local data.\nsection: Development\nslug: local-development\norder: 60\n---\n\n# Local development\n\nRun the development server from the app directory:\n\n```sh\nnpx ignotum dev\n```\n\nThe command requires `client/index.tsx` and `server/schema.ts`. It updates generated code, starts\nthe Web and function runtimes, and prints the local URL. The default is\n<http://127.0.0.1:3210>.\n\nClient changes use hot reload. Changes to server `.ts` files reload the function modules and\nrefresh active queries. Adding or removing a function module also updates the API references.\n\nThe development server uses one local database for the app directory. A second server for the same\napp fails with a message that identifies the process holding the database lock.\n\n## Options\n\nUse flags to change the listener or open a browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------------------------------------ |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. The command fails if it is in use. |\n| `--open` | off | Open the app in the default browser after startup. |\n\n## Local data\n\nLocal data survives restarts and lives under `.ignotum/dev`. The generated project ignores\n`.ignotum` in Git.\n\nStop the development server before resetting the database:\n\n```sh\nnpx ignotum dev db reset\n```\n\nThe command refuses to reset data while a live development server holds the lock. A successful\nreset removes the local database. The next start creates an empty database from the current schema.\n\nLocal development does not reproduce every hosted resource limit or the hosted function runtime.\nRun the typecheck and review [Limits](limits.md) before deployment.\n\nNext: [Generated code](generated-code.md)\n"],
34294
+ ["mutations.md", "---\ntitle: Mutations\ndescription: Define and call typed mutations.\nsection: Functions\nslug: mutations\norder: 23\n---\n\n# Mutations\n\nA mutation reads and changes application data in one atomic operation. Web code calls it through a\ngenerated reference and receives a typed value or application error.\n\n## Define a mutation\n\nImport `mutation` and schema-bound `values` from `_generated/server.ts`:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const create = mutation({\n args: { text: values.string() },\n returns: values.id(\"todos\"),\n\n handler: function* (ctx, { text }) {\n return yield* ctx.db.insert(\"todos\", {\n text,\n completed: false,\n });\n },\n});\n```\n\nMutation `ctx.db` includes all read methods plus `insert`, `patch`, `replace`, and `delete`. See\n[Writing data](writing-data.md) for their exact behavior.\n\nAll reads and writes in one handler belong to one transaction. A successful return commits them\ntogether. An application error, thrown exception, invalid return, limit failure, or platform\nfailure commits none of them. Other mutations for the same app do not interleave with the handler.\n\n## Call a mutation from Web code\n\n`useMutation` takes the generated reference and returns a stable typed function:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\n\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe promise resolves with a settled `Result` after the mutation commits or returns a declared\napplication error. It rejects for client or protocol failures, including unsupported argument\nvalues, an unavailable function, or a hosted limit that prevents the call from starting.\n\n## Handle the result\n\nThe client `Result` export contains `match`. Server operations such as `Result.fail`,\n`Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available in browser code.\n\nApplication errors use the `error` handler. A single function receives their union. An object must\nhandle every `_tag` separately. The optional `internalError` handler receives an internal failure's\nrequest ID. Without that handler, `Result.match` throws the error so an error boundary can handle\nit. See [Errors](errors.md).\n\n## Reconnects and retries\n\nThe client assigns one invocation ID to a mutation call. If the connection drops before the result\narrives, it keeps the promise pending and sends the same invocation after a matching reconnect.\nIgnotum returns a retained result instead of applying the mutation twice.\n\nA deployment change reloads the page and does not replay pending work against different function\ncode. Retry records have retention limits and are not permanent duplicate protection for business\noperations. See [Guarantees](guarantees.md) and [Limits](limits.md).\n\nSuccessful mutations can refresh active queries after the commit. The mutation result can reach\nits caller before every affected query renders its next value.\n\nNext: [Files](files.md)\n"],
34295
+ ["pagination.md", "---\ntitle: Pagination\ndescription: Return cursor-based pages and load them from Web code.\nsection: Database\nslug: pagination\norder: 16\n---\n\n# Pagination\n\nA paginated query accepts `values.pagination()` and returns `values.page(...)`:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n args: {\n completed: values.boolean(),\n pagination: values.pagination(),\n },\n returns: values.page(Todo),\n\n handler: function* (ctx, { completed, pagination }) {\n return yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", completed))\n .order(\"desc\")\n .paginate(pagination);\n },\n});\n```\n\nThe result has `items` and `nextCursor`. A `null` cursor means the query was exhausted. Pass a\nnon-null cursor back with the same table, index, range, and order. A cursor from another query is\nrejected. Treat cursors as opaque strings.\n\nPage sizes are integers from 1 through 1,000. A page returns no more than the requested size.\n\n## Load pages in Web code\n\n`usePaginatedQuery` supplies the cursor and page size. Callers pass only the function's other\narguments:\n\n```tsx\nimport { Result, usePaginatedQuery } from \"ignotum/client\";\n\nconst events = usePaginatedQuery(api.events.list, {\n project: \"api\",\n level: \"error\",\n});\n\nreturn Result.match(events, {\n pending: () => <p>Loading...</p>,\n value: ({ items, loadMore, status }) => (\n <>\n {items.map((event) => (\n <p key={event.id}>{event.message}</p>\n ))}\n {status !== \"Exhausted\" && (\n <button disabled={status === \"LoadingMore\"} onClick={loadMore}>\n {status === \"LoadingMore\" ? \"Loading...\" : \"Load more\"}\n </button>\n )}\n </>\n ),\n});\n```\n\nThe default page size is 20. Pass an options object as the third argument to use a value from 1\nthrough 1,000:\n\n```tsx\nconst events = usePaginatedQuery(api.events.list, { project: \"api\" }, { pageSize: 50 });\n```\n\nIf pagination is the query's only argument, omit the second argument. `Query.skip` is accepted in\nplace of arguments and keeps the result pending.\n\nThe successful value has these fields:\n\n| Field | Meaning |\n| ------------ | --------------------------------------------- |\n| `items` | Items from every loaded page, in query order. |\n| `loadMore()` | Requests one more page. |\n| `status` | `CanLoadMore`, `LoadingMore`, or `Exhausted`. |\n\nEarlier items remain visible while another page loads. Changing the query arguments or page size\nstarts again at the first page.\n\n## Live page behavior\n\nPages do not freeze the database at the first page's revision. Each call reads current committed\nstate and continues after its cursor position. Data that moves across that position between calls\ncan appear on a different page or no longer appear in the remaining pages.\n\nEach loaded page remains subscribed. Use item IDs when rendering and allow items to move in a\nchanging list. See [Guarantees](guarantees.md) for the complete pagination contract.\n"],
34296
+ ["preact.md", "---\ntitle: Preact\ndescription: Use the Preact components, hooks, events, and types exposed by Ignotum.\nsection: Web\nslug: preact\norder: 42\n---\n\n# Preact\n\n`ignotum/client` supplies Preact's JSX runtime. Import components, hooks, and types from this entry\npoint instead of adding a second UI runtime.\n\nThe main exports are:\n\n| Area | Exports |\n| ----------- | --------------------------------------------------------------------------------------- |\n| Elements | `Fragment`, `createElement`, `h`, `cloneElement`, `createContext`, `createRef` |\n| Components | `Component`, `isValidElement`, `toChildArray` |\n| Hooks | `useState`, `useEffect`, `useMemo`, `useCallback`, `useReducer`, `useRef`, `useContext` |\n| Other hooks | `useId`, `useLayoutEffect`, `useImperativeHandle`, `useDebugValue`, `useErrorBoundary` |\n\nThe entry point also exports Preact's component, event, JSX, ref, and hook types. TypeScript usually\ninfers them from JSX and hook calls.\n\nUse the JSX `class` attribute. Event handlers receive Preact's typed DOM events:\n\n```tsx\nimport { useState, type TargetedInputEvent } from \"ignotum/client\";\n\nfunction Search() {\n const [text, setText] = useState(\"\");\n\n const update = (event: TargetedInputEvent<HTMLInputElement>) => {\n setText(event.currentTarget.value);\n };\n\n return <input class=\"rounded border px-3 py-2\" value={text} onInput={update} />;\n}\n```\n\nIgnotum-specific browser exports include `app`, `Files`, `Query`, `Result`, `useQuery`,\n`usePaginatedQuery`, and `useMutation`. Their complete workflows are documented in\n[Files](files.md), [Queries](queries.md), and [Mutations](mutations.md).\n\nNext: [Styling](styling.md)\n"],
34297
+ ["project-structure.md", "---\ntitle: Project structure\ndescription: Place Web code, functions, data definitions, and shared code in an Ignotum app.\nsection: Start\nslug: project-structure\norder: 4\n---\n\n# Project structure\n\nAn Ignotum project separates browser code from function code while keeping both in one app:\n\n```text\nclient/\n index.tsx\nserver/\n schema.ts\n todos.ts\nshared/\n_generated/\npublic/\n```\n\nOnly `client/index.tsx` and `server/schema.ts` are required. The other paths appear when the app\nneeds them.\n\n## `client`\n\n`client` contains the Web part. Its `index.tsx` file default-exports one `app(...)` definition:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nClient modules import browser APIs and UI exports from `ignotum/client`. They may also import\n`_generated/api.ts`, `_generated/types.ts`, other client modules, shared modules, and\nbrowser-compatible dependencies. They cannot import server modules or `ignotum/server`.\n\n## `server`\n\n`server/schema.ts` defines the database schema. Other `.ts` files directly inside `server` may\nexport queries and mutations. Their file names become generated API modules. For example,\n`server/todos.ts` produces references such as `api.todos.list`.\n\nSchema definitions import `defineSchema` from `ignotum/server`. Functions import schema-bound\n`query`, `mutation`, and `values` from `@/_generated/server.js`. Server modules may import other\nserver modules, shared modules, generated server and data types, and supported dependencies. They\ncannot import client modules or `ignotum/client`.\n\n## `shared`\n\n`shared` contains code that is valid in both runtimes. Shared modules cannot depend on browser-only\nor function-only APIs. Put a type, validator-independent helper, or constant here only when both\nsides use it.\n\n## `_generated`\n\nIgnotum writes generated function references, schema-bound builders, and data types to\n`_generated`. Do not edit these files. See [Generated code](generated-code.md).\n\n## `public`\n\nThe optional top-level `public` directory contains fixed files that keep their relative paths as\nWeb URLs. It is separate from application files stored in database documents. See\n[Public assets](public-assets.md).\n\nNext: [Schema](schema.md)\n"],
34298
+ ["public-assets.md", "---\ntitle: Public assets\ndescription: Add a favicon and fixed files to the Web part.\nsection: Web\nslug: public-assets\norder: 44\n---\n\n# Public assets\n\nPublic assets belong to a deployment and keep stable URL paths. They are different from\n[application files](files.md), which belong to database documents and require a current query\nresult.\n\n## Favicon\n\nAdd `client/icon.svg` to use an SVG favicon. Ignotum discovers this exact path. Without the file,\nthe page has no favicon link.\n\nThe icon must be a regular file. Other SVG files cannot go in `public`, but imported SVG assets may\nstill be part of client code.\n\n## `public`\n\nPut files that need fixed URLs in a top-level `public` directory next to `client` and `server`:\n\n```text\npublic/\n images/\n logo.png\n manual.pdf\n```\n\nThese files become `/images/logo.png` and `/manual.pdf`. `client/public` is not supported.\n\nIgnotum accepts AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. It checks the file signature against the\nextension. Entries must be regular files or directories, not symbolic links. Each path segment may\ncontain letters, numbers, `.`, `_`, `~`, or `-`. The first segment cannot be `_ignotum`, which is\nreserved for platform paths.\n\nEach file is limited to 16 MiB, and all public files count toward the deployment limits. See\n[Limits](limits.md).\n"],
34299
+ ["queries.md", "---\ntitle: Queries\ndescription: Define, call, and subscribe to typed queries.\nsection: Functions\nslug: queries\norder: 22\n---\n\n# Queries\n\nA query reads application data and returns a typed result. Web code calls it through a generated\nreference and stays subscribed to relevant data changes.\n\n## Define a query\n\nImport `query` and schema-bound `values` from `_generated/server.ts`. Define the arguments, return\nvalue, and handler together:\n\n```ts\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n args: { id: values.id(\"todos\") },\n returns: values.doc(\"todos\"),\n\n handler: function* (ctx, { id }) {\n return yield* ctx.db.get(\"todos\", id);\n },\n});\n```\n\nQueries have a read-only `ctx.db`. Use [Reading data](reading-data.md) for document and table reads,\nand [Indexes](indexes.md) for filtered and ordered results.\n\n## Call a query from Web code\n\nThe exported function above becomes `api.todos.get`. Pass that reference to `useQuery`:\n\n```tsx\nimport { Query, Result, useQuery } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction Todo({ id }: { id: Id<\"todos\"> | undefined }) {\n const todo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n\n return Result.match(todo, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value.text}</p>,\n error: {\n DocumentNotFound: () => <p>The todo no longer exists.</p>,\n },\n });\n}\n```\n\nArgument-free queries take only the reference:\n\n```tsx\nconst todos = useQuery(api.todos.list);\n```\n\nQueries with arguments require the second parameter. The generated reference supplies its type.\n`Query.skip` keeps the hook pending and opens no subscription. Changing from `Query.skip` to\narguments starts the query. Changing the arguments switches the subscription and returns pending\nuntil the new query has a result.\n\n## Handle the result\n\n`useQuery` first returns a pending `QueryResult`, then a value or application error.\n`Result.match` requires handlers for the states and declared errors in its type:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nUse one `error` function when the UI treats all application errors alike. Use an object keyed by\n`_tag` when errors need different handling. `internalError` is optional. Without it, an\n`InternalServerError` is thrown to the nearest UI error boundary. See [Errors](errors.md).\n\n## Realtime behavior\n\nThe hook keeps its subscription until the component stops using it or its reference and arguments\nchange. Components using the same query and arguments share one browser subscription.\n\nAfter a successful mutation, Ignotum refreshes active queries whose recorded reads may have\nchanged. A document lookup responds to writes for that document. An indexed range responds to\nchanges inside its relevant range. A full table query responds to writes in that table.\n\nA lost connection does not change a settled query back to pending. The client keeps the latest\nresult, reconnects with an increasing delay capped at five seconds, then recreates active\nsubscriptions. A deployment change reloads the page.\n\nUse [Pagination](pagination.md) for paginated query definitions and `usePaginatedQuery`. Read\n[Guarantees](guarantees.md) for consistency, reconnect, and update behavior.\n\nNext: [Mutations](mutations.md)\n"],
34300
+ ["quickstart.md", "---\ntitle: Quickstart\ndescription: Create, understand, and deploy a small Ignotum app.\nsection: Start\nslug: quickstart\norder: 2\n---\n\n# Quickstart\n\nIgnotum requires Node.js 22.18 or newer. Create the generated counter app and start it:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The page shows a counter backed by an Ignotum database.\n\n## Find the app parts\n\nThe files used in this guide are:\n\n```text\nclient/\n index.tsx\nserver/\n counter.ts\n schema.ts\nshared/\n utils.ts\n_generated/\n```\n\n`server/schema.ts` defines stored data. `server/counter.ts` defines the query and mutation.\n`client/index.tsx` defines the Web app and calls those functions. `shared/utils.ts` is available to\nboth runtimes. Ignotum owns the files under `_generated`.\n\n## Define data\n\nThe generated schema has one table:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\nThe schema supplies runtime validation and the document types used by functions and Web code.\n\n## Define a query\n\n`server/counter.ts` exports `get`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n```\n\nCode generation gives this function the reference `api.counter.get`. The Web app subscribes to it\nwith `useQuery`:\n\n```tsx\nconst count = useQuery(api.counter.get);\n```\n\n`Result.match` renders the pending state and the returned value:\n\n```tsx\n{\n Result.match(count, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value}</p>,\n });\n}\n```\n\n## Define a mutation\n\nThe generated `increment` mutation either inserts the first counter document or updates the\nexisting one:\n\n```ts\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + 1;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nWeb code obtains a typed caller from the generated reference:\n\n```tsx\nconst increment = useMutation(api.counter.increment);\n\n<button type=\"button\" onClick={() => void increment()}>\n Increment\n</button>;\n```\n\nClick the button in one browser window. The mutation commits the new value, then the active query\nreceives an updated result. Open a second window to see both subscriptions follow the same data.\n\n## Deploy the app\n\nSet the API token you were issued, then choose a globally available app slug:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\nnpx ignotum deploy --app my-app\n```\n\nThe command prints the hosted URL and links this directory to the hosted app. Later deployments use\nthe saved link:\n\n```sh\nnpx ignotum deploy\n```\n\nRead [Deploy an app](deploy-app.md) for token handling, slug rules, and deployment failure behavior.\n\nNext: [How Ignotum works](how-ignotum-works.md)\n"],
34301
+ ["reading-data.md", "---\ntitle: Reading data\ndescription: Read individual documents and table results.\nsection: Database\nslug: reading-data\norder: 13\n---\n\n# Reading data\n\nQuery and mutation handlers read documents through `ctx.db`. A read can name one document, scan a\ntable, or select a declared index. Ignotum does not turn an unindexed filter into a hidden table\nscan. Use an [index](indexes.md) for a filtered database read, or read the table and filter it when\nthe table is intentionally small.\n\n## One document\n\nUse `find` when absence is expected:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Doc<\"todos\"> | undefined\n```\n\nUse `get` when absence is an application error path:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Doc<\"todos\">\n```\n\nA missing `get` fails with `DocumentNotFound`. The error contains `table` and `id`. Catch it to\nrecover or map it to a public error. If it remains in the handler, it becomes part of that\nfunction's typed application errors.\n\nIDs are scoped to their tables at compile time. A `users` ID cannot be passed to a `todos` read.\n\n## A table\n\nStart a table query with `ctx.db.query`:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nA table query is ordered by `createdAt`, then `id`, both ascending. Reverse both fields with\n`order(\"desc\")`:\n\n```ts\nconst newest = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\nUse a table query when the function needs a table-wide result. Any write in that table can cause a\nsubscribed query that used the result to refresh.\n\n## Result methods\n\nFinish a table or index query with one method:\n\n| Method | Result |\n| ------------------- | ------------------------------------------------------------------------------ |\n| `collect()` | Every matching document, within hosted collection limits. |\n| `take(count)` | At most `count` documents. `count` may be 0 through 1,000. |\n| `first()` | The first document, or `undefined`. |\n| `unique()` | The only document, or `undefined`. More than one match is an internal failure. |\n| `paginate(options)` | One page and an opaque cursor. |\n\n`unique()` checks a result. It does not make the index unique and does not prevent duplicate field\nvalues. Use `first()` when multiple matches are valid.\n\nPrefer `take` or `first` when the function only needs a bounded result. `collect` is capped at\n1,000 documents when hosted and may hit byte limits first.\n\n## Reads inside a mutation\n\nA mutation sees writes it made earlier in the same handler, including through table and index\nqueries:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\nconst changed = yield * ctx.db.get(\"todos\", todo.id);\n```\n\nNo other mutation can interleave with those operations for the same app. See\n[Writing data](writing-data.md) and [Guarantees](guarantees.md).\n"],
34302
+ ["schema.md", "---\ntitle: Schema\ndescription: Define tables, fields, IDs, and stored document types.\nsection: Database\nslug: schema\norder: 11\n---\n\n# Schema\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }).index(\"by_completed\", [\"completed\"]),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n`createdAt` is set when Ignotum inserts the document. `updatedAt` starts at the same time and\nadvances on every successful patch or replacement. Both are JavaScript `Date` values in function\nand Web code.\n\nChain `index` after a table to declare an ordered index. [Indexes](indexes.md) covers field rules,\nranges, and ordering.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n\n## Schema changes\n\nLocal development updates tables and indexes as the schema changes. Existing documents still have\nto match the current field validators when a function reads them.\n\nFor hosted apps, the first active deployment fixes table names, field names, and field validators.\nLater deployments may add, remove, rename, or reorder indexes. They may also change functions and\nWeb code. A deployment that changes the stored table or field schema is built and uploaded, but\nactivation fails with `SchemaIncompatible`, leaving the current deployment active.\n\nSchema migrations are not part of the current public API. Plan the first hosted schema with this\nrestriction in mind.\n\nNext: [Queries](queries.md)\n"],
34303
+ ["styling.md", "---\ntitle: Styling\ndescription: Style Web code with Tailwind CSS or imported CSS files.\nsection: Web\nslug: styling\norder: 43\n---\n\n# Styling\n\nIgnotum loads Tailwind CSS automatically. An app does not need a Tailwind configuration file or a\nframework stylesheet.\n\nUse utility classes through JSX's `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nCustom CSS is an ordinary client module. Give the file any name and import it from client code:\n\n```tsx\nimport \"./calendar.css\";\n```\n\nNext: [Public assets](public-assets.md)\n"],
34304
+ ["values.md", "---\ntitle: Values\ndescription: Use runtime validators and their matching TypeScript types.\nsection: Database\nslug: values\norder: 12\n---\n\n# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. The same definition supplies a TypeScript type and a runtime check. Do not substitute a\nTypeScript-only interface where Ignotum asks for a validator.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.file(options?)` | `FileValue` | Accepts configured image formats and a byte limit. See [Files](files.md). |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\nDates cross the Web and function boundary as `Date` instances. Ignotum rejects invalid dates.\n`values.number()` rejects `NaN`, `Infinity`, and `-Infinity`. Use `values.integer()` when fractional\nvalues should also be rejected.\n\n`values.file()` accepts JPEG, PNG, WebP, AVIF, and GIF by default. Pass `formats` to allow a\nnon-empty subset and `maxBytes` to set a positive limit up to 10 MiB. [Files](files.md) covers the\ncomplete file workflow and the places where file values are allowed.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method returns a new validator and leaves the source unchanged. The next method works from the\ncurrent result. Added fields can be picked, omitted, or overridden immediately. An omitted name can\nbe added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\nFixed object validators describe named fields. Use `values.record(value)` when keys are not known\nin advance.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n\nWrapping a validator in both supports omission and an explicit `null`:\n\n```ts\nvalues.optional(values.nullable(values.date()));\n```\n"],
34305
+ ["web.md", "---\ntitle: Web overview\nlabel: Overview\ndescription: Define the browser-facing part of an Ignotum app.\nsection: Web\nslug: web\norder: 40\n---\n\n# Web overview\n\nThe Web part of an Ignotum app lives in `client`. It renders the browser UI and calls queries and\nmutations through `_generated/api.ts`.\n\n`client/index.tsx` is the entry. It default-exports an `app(...)` definition with the page title and\nroot Preact component. Ignotum supplies the JSX runtime and Tailwind CSS.\n\n- [App entry](app-entry.md) documents `client/index.tsx` and the `app(...)` definition.\n- [Preact](preact.md) lists the UI exports and import rules.\n- [Styling](styling.md) covers Tailwind CSS and custom CSS.\n- [Public assets](public-assets.md) covers the favicon and files with fixed URL paths.\n- [Queries](queries.md) and [Mutations](mutations.md) cover the generated API and browser hooks.\n\nClient modules may import `_generated/api.ts`, `_generated/types.ts`, other client modules, shared\nmodules, and browser-compatible dependencies. They cannot import files under `server`,\n`_generated/server.ts`, `ignotum/server`, Node built-ins, or native add-ons.\n\nNext: [App entry](app-entry.md)\n"],
34306
+ ["writing-data.md", "---\ntitle: Writing data\ndescription: Insert, patch, replace, and delete documents in a mutation.\nsection: Database\nslug: writing-data\norder: 14\n---\n\n# Writing data\n\nOnly mutation handlers can write. Their `ctx.db` has four write methods and all methods from\n[Reading data](reading-data.md).\n\n## Insert\n\n`insert` creates a document and returns its table-specific ID:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n```\n\nSupply every required application field and omit optional fields that have no value. Ignotum sets\n`id`, `createdAt`, and `updatedAt`. Those system fields are not valid insert input.\n\n## Patch\n\n`patch` changes the supplied fields and leaves the rest alone:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nThe patch input is type-checked against the table, and every field is optional. A matching\ndocument keeps its `id` and `createdAt`; its `updatedAt` advances.\n\n## Replace\n\n`replace` supplies a new complete set of application fields:\n\n```ts\nyield *\n ctx.db.replace(\"todos\", args.id, {\n text: \"Build an app\",\n completed: false,\n });\n```\n\nEvery required field must be present. A matching document keeps its `id` and `createdAt`; its\n`updatedAt` advances.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Missing targets\n\n`patch`, `replace`, and `delete` fail with `DocumentNotFound` when the target document does not\nexist. The error contains the target `table` and `id`. Catch it directly or map it to a public\napplication error as described in [Errors](errors.md).\n\nUse `find` first when a missing target should be ignored:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\nif (todo !== undefined) yield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Transaction behavior\n\nA mutation reads one logical state, sees its own earlier writes, and commits all successful writes\ntogether. Other mutations for the same app do not interleave with it.\n\n```ts\nexport const completeOldest = mutation({\n handler: function* (ctx) {\n const todo = yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", false))\n .first();\n\n if (todo !== undefined) {\n yield* ctx.db.patch(\"todos\", todo.id, { completed: true });\n }\n },\n});\n```\n\nThe commit happens only after the handler returns a valid success value. A declared application\nerror, thrown exception, invalid return, limit failure, or platform failure commits none of the\nmutation's writes.\n\nIgnotum validates stored fields and maintains declared indexes as part of the same atomic write.\nIf the mutation would exceed the hosted app storage quota, the whole mutation rolls back.\n\nSuccessful mutations cause active queries to refresh when their recorded reads may have changed.\nThe mutation result can reach its caller before every affected query has delivered its next\nsnapshot, so do not treat subscription rendering as part of the mutation promise. See\n[Guarantees](guarantees.md).\n"]
34307
+ ];
34308
+ const withoutFrontmatter = (content) => content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
32994
34309
  const agentAppFiles = [
32995
34310
  {
32996
- content: "# AGENTS.md\n\nThis is an Ignotum app. Before changing it, read `.agents/skills/ignotum/SKILL.md`.\n\nThe skill explains how to inspect the app, which bundled reference to read, and how to check\nyour changes.\n",
34311
+ content: AGENTS_default,
32997
34312
  path: "AGENTS.md"
32998
34313
  },
32999
34314
  {
33000
- content: "---\nname: ignotum\ndescription: Build and modify an Ignotum app. Use when working on its schema, server functions, generated API, client UI, or local development workflow.\n---\n\n# Working on an Ignotum app\n\nAn Ignotum app defines a database schema, server functions, and a JSX client. Ignotum supplies the\ndatabase runtime, generated bindings, realtime query updates, Vite dev server, JSX runtime, and\nTailwind setup.\n\nThe current release supports local development. Do not assume that authentication, files, actions,\nworkflows, scheduled jobs, or production deployment APIs exist.\n\n## Start with the app\n\nInspect `server/schema.ts`, the function modules in `server`, the files in `client`, `package.json`,\nand `tsconfig.json` before changing code.\n\nThe required `client/index.tsx` default-exports `app({ title, component })` from `ignotum/client`.\nKeep the title as a non-empty quoted string. An optional `client/icon.svg` is discovered as the app\nfavicon. Do not add another metadata file or import Ignotum's Tailwind stylesheet.\n\nTreat `_generated` as compiler output. Read it when you need to understand an inferred type, but do\nnot edit it. After code changes, regenerate the bindings and run the typechecker:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nFor normal Ignotum app work, do not add an HTML file, Vite configuration, Tailwind configuration,\nAPI routes, or direct database setup.\n\n## Read the relevant reference\n\nThe bundled references are the Ignotum user guides. Read only the guides needed for the task:\n\n- For app creation and installation, read [getting started](references/getting-started.md) or\n [manual setup](references/manual-setup.md).\n- For tables, fields, IDs, and generated document types, read\n [schema syntax](references/schema.md).\n- For validators and their TypeScript types, read [values](references/values.md).\n- For queries, mutations, database access, Results, and application errors, read\n [server functions](references/server-functions.md).\n- For hooks, query state, mutations, JSX, and Tailwind, read [client](references/client.md).\n- For development commands, code generation, flags, and local data, read\n [dev server](references/dev-server.md).\n- For the complete guide list, read the [documentation index](references/index.md).\n\n## Rules that cross guide boundaries\n\n- Define tables in `server/schema.ts`.\n- Put queries and mutations in TypeScript files directly inside `server`.\n- Import server builders from `@/_generated/server.js` and client references from\n `@/_generated/api.js`. Keep the `.js` suffix.\n- Put code used by both the client and server in `shared` and import it through `@/shared`.\n- Never edit `_generated`.\n- Use JSX and hooks from `ignotum/client`, not React or Preact packages directly.\n- Keep the app inside Ignotum's current model unless the user explicitly asks to move beyond it.\n",
34315
+ content: SKILL_default,
33001
34316
  path: ".agents/skills/ignotum/SKILL.md"
33002
34317
  },
33003
- ...[
33004
- ["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Public files\n\nPut files that need their own URL in a top-level `public` directory next to `client` and `server`.\nThe directory structure becomes the URL structure. For example, `public/documents/manual.pdf` is\navailable at `/documents/manual.pdf` after deployment.\n\nPublic files are limited to AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. Ignotum checks that a file's\ncontents match its extension and rejects symbolic links, executable or code formats, and paths\nunder the reserved `_ignotum` name. Use `client/icon.svg` for the app favicon; arbitrary SVG files\nare not accepted in `public`. Put the directory at the app root—`client/public` is rejected.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
33005
- ["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains the SPA shell, generated assets, and validated files from the top-level\n`public` directory. The server output contains one bundle for every exported query or mutation.\nIgnotum writes both under `.ignotum/build` only after the complete build succeeds. If a rebuild\nfails, the previous successful output stays in place.\n\nUploads stream from disk. If an upload is interrupted after it begins, Ignotum leaves the active\ndeployment unchanged and prints the incomplete deployment ID. A rejected upload reports the\nAPI's reason. A successful deploy prints the app URL and deployment ID.\n\nThe CLI uses `https://api.ignotum.cloud` by default. Set `IGNOTUM_API_URL` only when targeting a\ndifferent Ignotum API, such as a local or development deployment.\n\nIgnotum rejects deployments that exceed the hosted file or artifact size limits. It also removes\nunfinished uploads and older inactive deployments after their retention periods. See\n[Limits](limits.md) for the current values.\n"],
33006
- ["dev-server.md", "# Dev server\n\nRun the dev server from the app root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/index.tsx\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, reads the `app(...)` definition from\n`client/index.tsx`, loads Tailwind automatically, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, Tailwind configuration, or framework stylesheet.\nAdd an optional `client/icon.svg` and Ignotum uses it as the favicon automatically. With no icon\nfile, the HTML contains no favicon link.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
33007
- ["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
33008
- ["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, database access, and errors.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n- [Deploy](deploy.md) creates or links an app, then uploads and activates a deployment.\n- [Limits](limits.md) lists hosted limits for functions, data, realtime updates, and deployments.\n"],
33009
- ["limits.md", "# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries one query execution when app data changes while it runs. That execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents and the records Ignotum keeps to process mutations safely.\nDeployment files do not count as stored app data.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
33010
- ["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to continue building the app.\n"],
33011
- ["schema.md", "# Schema syntax\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n"],
33012
- ["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database reads\n\nUse `find` when a missing document is a normal result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with a typed `DocumentNotFound` value containing `table` and `id`. Collect a\nwhole table through a query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nQuery handlers only receive read methods.\n\nHosted apps limit document size, collection reads, function runtime, and stored app data. See\n[Limits](limits.md) for the current values.\n\n## Database writes\n\nMutation handlers receive the read methods and these writes:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n\nyield * ctx.db.patch(\"todos\", id, { completed: true });\n\nyield *\n ctx.db.replace(\"todos\", id, {\n text: \"Build an app\",\n completed: false,\n });\n\nyield * ctx.db.delete(\"todos\", id);\n```\n\n`patch` changes only supplied fields. `replace` requires every non-optional table field. Ignotum\nrolls back a mutation when its handler fails with a typed application error or encounters an\ninternal failure.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n"],
33013
- ["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\n`values.doc` is available on the schema-bound `values` exported by `_generated/server.ts`. It is\nnot available while defining `server/schema.ts`, because the table definitions are still being\ncreated there.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method works from the result of the previous method. Added fields can be picked, omitted, or\noverridden immediately. An omitted name can be added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n"]
33014
- ].map(([name, content]) => ({
33015
- content,
34318
+ ...references.map(([name, content]) => ({
34319
+ content: withoutFrontmatter(content),
33016
34320
  path: `.agents/skills/ignotum/references/${name}`
33017
34321
  }))
33018
34322
  ];
@@ -33203,6 +34507,17 @@ Open <http://127.0.0.1:3210>.
33203
34507
  The app generator creates \`_generated\`. The dev server checks those files before it starts
33204
34508
  and updates them when the schema or server functions change.
33205
34509
 
34510
+ ## Deploy
34511
+
34512
+ Set \`IGNOTUM_API_TOKEN\`, then choose a unique slug on the first deployment:
34513
+
34514
+ \`\`\`sh
34515
+ npx ignotum deploy --app my-app
34516
+ \`\`\`
34517
+
34518
+ The command prints the hosted URL and records the app link in \`.ignotum/app.json\`. Later
34519
+ deployments use \`npx ignotum deploy\` without the \`--app\` flag.
34520
+
33206
34521
  ## App files
33207
34522
 
33208
34523
  - \`server/schema.ts\` defines the database tables.
@@ -33212,6 +34527,7 @@ and updates them when the schema or server functions change.
33212
34527
  - Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
33213
34528
  - \`shared/utils.ts\` contains code shared across the app.
33214
34529
  - \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
34530
+ - \`.agents/skills/ignotum\` contains the app skill and references used by coding agents.
33215
34531
 
33216
34532
  Run the typechecker after a change:
33217
34533
 
@@ -33220,6 +34536,9 @@ npx ignotum codegen
33220
34536
  npx tsc --noEmit
33221
34537
  \`\`\`
33222
34538
 
34539
+ The [Ignotum documentation](https://docs.ignotum.cloud) covers schema values, indexes, queries,
34540
+ mutations, client hooks, deployment, guarantees, and hosted limits.
34541
+
33223
34542
  ## Claude Code
33224
34543
 
33225
34544
  If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it