ignotum 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.mjs +825 -403
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
- package/dist/runtime/api-CAgKDij7.js.map +1 -0
- package/dist/runtime/{api-5XSrIeqW.d.ts → api-Cv3hMbzo.d.ts} +4 -4
- package/dist/runtime/client.d.ts +39 -9
- package/dist/runtime/client.js +339 -60
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +154 -0
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +1 -0
- package/dist/runtime/id-Bt9XWRGL.js +423 -0
- package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-CK-maMgN.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +2 -2
- package/dist/runtime/id-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/{index-CrWg4Z0y.d.ts → index-D54flWtH.d.ts} +9 -5
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +9 -6
- package/dist/runtime/internal/host.js +15 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-D-R9NR61-CpIoHFoI.d.ts → pagination-DnKg3dkI-r5ZUxBBx.d.ts} +31 -6
- package/dist/runtime/pagination-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
- package/dist/runtime/{schema-B9XxyRO8.js → schema-1Zs03-iS.js} +5 -3
- package/dist/runtime/schema-1Zs03-iS.js.map +1 -0
- package/dist/runtime/server.d.ts +6 -4
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-avN7NkcU.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
- package/package.json +1 -1
- package/src/cli/agent-files.ts +6 -0
- package/src/client/hooks.ts +68 -0
- package/src/client/id.ts +259 -0
- package/src/client/index.ts +5 -2
- package/src/client/sync.ts +143 -10
- package/src/dev-runtime/functions.ts +111 -88
- package/src/dev-runtime/id.ts +175 -0
- package/src/dev-runtime/query-cache.ts +105 -0
- package/src/dev-runtime/sync.ts +149 -17
- package/src/server/index.ts +2 -0
- package/dist/runtime/api-DzcR7spt.js.map +0 -1
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js +0 -330
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +0 -1
- package/dist/runtime/file-C1abuMgd.js +0 -173
- package/dist/runtime/file-C1abuMgd.js.map +0 -1
- package/dist/runtime/pagination-CX5IPbEi.d.ts +0 -1
- package/dist/runtime/result-DIjKM-p4.d.ts +0 -1
- package/dist/runtime/schema-B9XxyRO8.js.map +0 -1
package/dist/cli/bin.mjs
CHANGED
|
@@ -3,10 +3,10 @@ import { createRequire, isBuiltin } from "node:module";
|
|
|
3
3
|
import * as NFS from "node:fs";
|
|
4
4
|
import { writeSync } from "node:fs";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
|
-
import * as Deferred from "effect/Deferred";
|
|
6
|
+
import * as Deferred$1 from "effect/Deferred";
|
|
7
7
|
import * as Effect$1 from "effect/Effect";
|
|
8
8
|
import { effectify } from "effect/Effect";
|
|
9
|
-
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Exit$1 from "effect/Exit";
|
|
10
10
|
import * as FileSystem$1 from "effect/FileSystem";
|
|
11
11
|
import * as Layer$1 from "effect/Layer";
|
|
12
12
|
import * as Path$3 from "effect/Path";
|
|
@@ -71,7 +71,7 @@ import * as RcRef from "effect/RcRef";
|
|
|
71
71
|
import * as Terminal$1 from "effect/Terminal";
|
|
72
72
|
import * as readline from "node:readline";
|
|
73
73
|
import * as Runtime from "effect/Runtime";
|
|
74
|
-
import { Array as Array$1, Brand, Cause, Config, ConfigProvider, 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, Redacted, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Struct, Terminal } from "effect";
|
|
74
|
+
import { Array as Array$1, Brand, Cause, Clock, Config, ConfigProvider, Context, Crypto, DateTime, Deferred, Effect, Effectable, Encoding, Exit, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Redacted, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Struct, Terminal } from "effect";
|
|
75
75
|
import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
76
76
|
import { customAlphabet } from "nanoid";
|
|
77
77
|
import { createAuthClient } from "better-auth/client";
|
|
@@ -371,11 +371,11 @@ const readableToPullUnsafe = (options) => {
|
|
|
371
371
|
latch.openUnsafe();
|
|
372
372
|
}
|
|
373
373
|
function onError(error) {
|
|
374
|
-
exit.current = Exit.fail(options.onError(error));
|
|
374
|
+
exit.current = Exit$1.fail(options.onError(error));
|
|
375
375
|
latch.openUnsafe();
|
|
376
376
|
}
|
|
377
377
|
function onEnd() {
|
|
378
|
-
exit.current = Exit.fail(Cause$1.Done());
|
|
378
|
+
exit.current = Exit$1.fail(Cause$1.Done());
|
|
379
379
|
latch.openUnsafe();
|
|
380
380
|
}
|
|
381
381
|
readable.on("readable", onReadable);
|
|
@@ -420,7 +420,7 @@ var StreamAdapter = class extends Readable {
|
|
|
420
420
|
}))).pipe(this.readLatch.whenOpen, Effect$1.provideContext(context), Effect$1.runFork);
|
|
421
421
|
this.fiber.addObserver((exit) => {
|
|
422
422
|
this.fiber = void 0;
|
|
423
|
-
if (Exit.isSuccess(exit)) this.push(null);
|
|
423
|
+
if (Exit$1.isSuccess(exit)) this.push(null);
|
|
424
424
|
else this.destroy(Cause$1.squash(exit.cause));
|
|
425
425
|
});
|
|
426
426
|
}
|
|
@@ -602,13 +602,13 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
602
602
|
};
|
|
603
603
|
};
|
|
604
604
|
const spawn = (command, spawnOptions) => Effect$1.callback((resume) => {
|
|
605
|
-
const deferred = Deferred.makeUnsafe();
|
|
605
|
+
const deferred = Deferred$1.makeUnsafe();
|
|
606
606
|
const handle = NodeChildProcess.spawn(command.command, command.args, spawnOptions);
|
|
607
607
|
handle.on("error", (error) => {
|
|
608
608
|
resume(Effect$1.fail(toPlatformError("spawn", error, command)));
|
|
609
609
|
});
|
|
610
610
|
handle.on("exit", (...args) => {
|
|
611
|
-
Deferred.doneUnsafe(deferred, Exit.succeed(args));
|
|
611
|
+
Deferred$1.doneUnsafe(deferred, Exit$1.succeed(args));
|
|
612
612
|
});
|
|
613
613
|
handle.on("spawn", () => {
|
|
614
614
|
resume(Effect$1.succeed([handle, deferred]));
|
|
@@ -687,15 +687,15 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
687
687
|
env,
|
|
688
688
|
stdio
|
|
689
689
|
}, process.platform)), Effect$1.fnUntraced(function* ([childProcess, exitSignal]) {
|
|
690
|
-
const exited = yield* Deferred.isDone(exitSignal);
|
|
690
|
+
const exited = yield* Deferred$1.isDone(exitSignal);
|
|
691
691
|
const killWithTimeout = withTimeout(childProcess, cmd, cmd.options);
|
|
692
692
|
if (exited) {
|
|
693
|
-
const [code] = yield* Deferred.await(exitSignal);
|
|
693
|
+
const [code] = yield* Deferred$1.await(exitSignal);
|
|
694
694
|
if (code !== 0 && Predicate$1.isNotNull(code)) return yield* Effect$1.ignore(killWithTimeout(killProcessGroup));
|
|
695
695
|
return yield* Effect$1.void;
|
|
696
696
|
}
|
|
697
697
|
if (!isReferenced) return yield* Effect$1.void;
|
|
698
|
-
return yield* killWithTimeout((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred.await(exitSignal)))).pipe(Effect$1.ignore);
|
|
698
|
+
return yield* killWithTimeout((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred$1.await(exitSignal)))).pipe(Effect$1.ignore);
|
|
699
699
|
}));
|
|
700
700
|
const pid = ProcessId(childProcess.pid);
|
|
701
701
|
childProcess.on("exit", (code) => {
|
|
@@ -717,14 +717,14 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
717
717
|
const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig);
|
|
718
718
|
const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig);
|
|
719
719
|
const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds);
|
|
720
|
-
const isRunning = Effect$1.map(Deferred.isDone(exitSignal), (done) => !done);
|
|
721
|
-
const exitCode = Effect$1.flatMap(Deferred.await(exitSignal), ([code, signal]) => {
|
|
720
|
+
const isRunning = Effect$1.map(Deferred$1.isDone(exitSignal), (done) => !done);
|
|
721
|
+
const exitCode = Effect$1.flatMap(Deferred$1.await(exitSignal), ([code, signal]) => {
|
|
722
722
|
if (Predicate$1.isNotNull(code)) return Effect$1.succeed(ExitCode(code));
|
|
723
723
|
const error = new globalThis.Error(`Process interrupted due to receipt of signal: '${signal}'`);
|
|
724
724
|
return Effect$1.fail(toPlatformError("exitCode", error, cmd));
|
|
725
725
|
});
|
|
726
726
|
const kill = (options) => {
|
|
727
|
-
return withTimeout(childProcess, cmd, options)((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred.await(exitSignal)))).pipe(Effect$1.asVoid);
|
|
727
|
+
return withTimeout(childProcess, cmd, options)((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred$1.await(exitSignal)))).pipe(Effect$1.asVoid);
|
|
728
728
|
};
|
|
729
729
|
return makeHandle({
|
|
730
730
|
pid,
|
|
@@ -29413,11 +29413,32 @@ defineId("conn", "ignotum/gateway/ConnectionId");
|
|
|
29413
29413
|
const RuntimeRequestNonce = defineId("nonce", "ignotum/runtime/RequestNonce");
|
|
29414
29414
|
const RequestId = defineId("req", "ignotum/runtime/RequestId");
|
|
29415
29415
|
const DevDatabaseLockId = defineId("lock", "ignotum/dev/DatabaseLockId");
|
|
29416
|
+
defineId("iid", "ignotum/id/AppUserId");
|
|
29417
|
+
defineId("idu", "ignotum/id/GlobalUserId");
|
|
29418
|
+
defineId("ids", "ignotum/id/SessionId");
|
|
29419
|
+
defineId("idr", "ignotum/id/RequestId");
|
|
29420
|
+
defineId("epoch", "ignotum/id/SessionEpoch");
|
|
29416
29421
|
//#endregion
|
|
29417
29422
|
//#region ../contracts/dist/runtime/identity.js
|
|
29418
29423
|
const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
|
|
29419
29424
|
const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
|
|
29420
29425
|
const AppStateRevision = Schema.Natural.pipe(Schema.brand("ignotum/hosted/AppStateRevision"));
|
|
29426
|
+
//#endregion
|
|
29427
|
+
//#region ../contracts/dist/versioned.js
|
|
29428
|
+
/** Registers the first revision of a long-lived format. */
|
|
29429
|
+
const initial = (schema) => schema;
|
|
29430
|
+
/** Adds one adjacent migration and keeps encoding on the new current revision. */
|
|
29431
|
+
const upgrade = (previous, current, migrate) => {
|
|
29432
|
+
const accepted = Schema.Union([previous, current]);
|
|
29433
|
+
const isCurrent = Schema.is(current);
|
|
29434
|
+
return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
|
|
29435
|
+
decode: SchemaGetter.transform((value) => {
|
|
29436
|
+
if (isCurrent(value)) return value;
|
|
29437
|
+
return migrate(value);
|
|
29438
|
+
}),
|
|
29439
|
+
encode: SchemaGetter.transform((value) => value)
|
|
29440
|
+
}));
|
|
29441
|
+
};
|
|
29421
29442
|
const FileFormat = Schema.Literals([
|
|
29422
29443
|
"jpeg",
|
|
29423
29444
|
"png",
|
|
@@ -29571,6 +29592,158 @@ const datePathsOf = (value) => {
|
|
|
29571
29592
|
};
|
|
29572
29593
|
const datePathsOfObject = (value) => datePathsOf(Schema.decodeUnknownSync(TransportObjectTypeSchema)(value));
|
|
29573
29594
|
//#endregion
|
|
29595
|
+
//#region ../contracts/dist/json.js
|
|
29596
|
+
const encodeJson = (value) => {
|
|
29597
|
+
if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
|
|
29598
|
+
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
|
|
29599
|
+
return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String$1.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
|
|
29600
|
+
};
|
|
29601
|
+
const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
|
|
29602
|
+
//#endregion
|
|
29603
|
+
//#region ../contracts/dist/runtime/result.js
|
|
29604
|
+
const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
|
|
29605
|
+
const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
|
|
29606
|
+
const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
|
|
29607
|
+
const ErrorBrand = "ignotum/error";
|
|
29608
|
+
Brand.nominal();
|
|
29609
|
+
const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
|
|
29610
|
+
var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: RequestId }) {};
|
|
29611
|
+
const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
|
|
29612
|
+
const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
|
|
29613
|
+
function makeResultEffectBase() {
|
|
29614
|
+
const Base = function() {};
|
|
29615
|
+
Base.prototype = Effectable.Prototype({
|
|
29616
|
+
label: "IgnotumResult",
|
|
29617
|
+
evaluate() {
|
|
29618
|
+
return this[ResultTypeId];
|
|
29619
|
+
}
|
|
29620
|
+
});
|
|
29621
|
+
return Base;
|
|
29622
|
+
}
|
|
29623
|
+
const ResultEffectBase = makeResultEffectBase();
|
|
29624
|
+
var ResultOperation = class ResultOperation extends ResultEffectBase {
|
|
29625
|
+
[ResultTypeId];
|
|
29626
|
+
constructor(effect, completed) {
|
|
29627
|
+
super();
|
|
29628
|
+
this[ResultTypeId] = effect;
|
|
29629
|
+
if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
|
|
29630
|
+
}
|
|
29631
|
+
catch(cases) {
|
|
29632
|
+
const effectCases = {};
|
|
29633
|
+
for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
|
|
29634
|
+
const runtimeEffect = this[ResultTypeId];
|
|
29635
|
+
const caught = Effect.catchTags(runtimeEffect, effectCases);
|
|
29636
|
+
return new ResultOperation(caught);
|
|
29637
|
+
}
|
|
29638
|
+
};
|
|
29639
|
+
var PendingQueryResult = class {
|
|
29640
|
+
[PendingResultTypeId] = PendingResultTypeId;
|
|
29641
|
+
};
|
|
29642
|
+
const inspectResult = (value) => {
|
|
29643
|
+
if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
|
|
29644
|
+
return value[CompletedResultTypeId];
|
|
29645
|
+
};
|
|
29646
|
+
const resultFromEffect = (effect) => new ResultOperation(effect);
|
|
29647
|
+
const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
|
|
29648
|
+
Function$1.dual(2, (result, matchers) => {
|
|
29649
|
+
if (result instanceof PendingQueryResult) {
|
|
29650
|
+
if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
|
|
29651
|
+
return matchers.pending();
|
|
29652
|
+
}
|
|
29653
|
+
if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
|
|
29654
|
+
const inspected = inspectResult(result);
|
|
29655
|
+
if (inspected?.type === "Success") {
|
|
29656
|
+
const value = inspected.value;
|
|
29657
|
+
return matchers.value(value);
|
|
29658
|
+
}
|
|
29659
|
+
if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
|
|
29660
|
+
const error = result[CompletedResultTypeId].error;
|
|
29661
|
+
if (error._tag === "InternalServerError") {
|
|
29662
|
+
const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
|
|
29663
|
+
const handler = matchers.internalError;
|
|
29664
|
+
if (handler === void 0) throw internalError;
|
|
29665
|
+
return handler(internalError);
|
|
29666
|
+
}
|
|
29667
|
+
const errorMatcher = matchers.error;
|
|
29668
|
+
if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
29669
|
+
if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
|
|
29670
|
+
const handlers = errorMatcher;
|
|
29671
|
+
const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
|
|
29672
|
+
if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
29673
|
+
return handler(error);
|
|
29674
|
+
});
|
|
29675
|
+
const documentNotFound = (table, id) => Brand.nominal()({
|
|
29676
|
+
_tag: "DocumentNotFound",
|
|
29677
|
+
table,
|
|
29678
|
+
id
|
|
29679
|
+
});
|
|
29680
|
+
//#endregion
|
|
29681
|
+
//#region ../contracts/dist/id.js
|
|
29682
|
+
const DevelopmentUsername = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9_-]{0,63}$/));
|
|
29683
|
+
const UserId = DevelopmentUsername.pipe(Schema.brand("ignotum/id/UserId"));
|
|
29684
|
+
const User = Schema.Struct({
|
|
29685
|
+
id: UserId,
|
|
29686
|
+
name: Schema.optional(Schema.String),
|
|
29687
|
+
email: Schema.optional(Schema.String),
|
|
29688
|
+
image: Schema.optional(Schema.String)
|
|
29689
|
+
});
|
|
29690
|
+
const ProfileField = Schema.Literals([
|
|
29691
|
+
"name",
|
|
29692
|
+
"email",
|
|
29693
|
+
"image"
|
|
29694
|
+
]);
|
|
29695
|
+
const ProfileFields = Schema.Array(ProfileField).check(Schema.isUnique());
|
|
29696
|
+
const IdRequired = Schema.TaggedStruct("IdRequired", {}).pipe(Schema.brand(ErrorBrand));
|
|
29697
|
+
const makeIdContext = (read) => ({
|
|
29698
|
+
current: () => resultFromEffect(read),
|
|
29699
|
+
require: () => resultFromEffect(read.pipe(Effect.flatMap((user) => user === null ? Effect.fail(IdRequired.make({})) : Effect.succeed(user))))
|
|
29700
|
+
});
|
|
29701
|
+
const SessionState = Schema.Struct({
|
|
29702
|
+
sessionEpoch: Schema.String,
|
|
29703
|
+
viewRevision: Schema.Natural,
|
|
29704
|
+
user: Schema.NullOr(User),
|
|
29705
|
+
validUntil: Schema.Finite
|
|
29706
|
+
});
|
|
29707
|
+
Schema.Struct({
|
|
29708
|
+
development: Schema.optional(Schema.Literal(true)),
|
|
29709
|
+
outcome: Schema.optional(Schema.Literals([
|
|
29710
|
+
"SignedIn",
|
|
29711
|
+
"ProfileUpdated",
|
|
29712
|
+
"Cancelled"
|
|
29713
|
+
])),
|
|
29714
|
+
...SessionState.fields,
|
|
29715
|
+
serverTime: Schema.Finite,
|
|
29716
|
+
expiresAt: Schema.Finite
|
|
29717
|
+
});
|
|
29718
|
+
const ReturnPath = Schema.String.check(Schema.makeFilter((value) => {
|
|
29719
|
+
if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return false;
|
|
29720
|
+
const url = new URL(value, "https://app.ignotum.invalid");
|
|
29721
|
+
return url.origin === "https://app.ignotum.invalid" && url.pathname !== "/_ignotum" && !url.pathname.startsWith("/_ignotum/");
|
|
29722
|
+
}, { message: "Return to a path within this app." }));
|
|
29723
|
+
const SessionRequest = Schema.Struct({
|
|
29724
|
+
intent: Schema.Literals(["signIn", "requestProfile"]),
|
|
29725
|
+
profile: ProfileFields,
|
|
29726
|
+
returnTo: ReturnPath,
|
|
29727
|
+
sessionEpoch: Schema.String
|
|
29728
|
+
});
|
|
29729
|
+
Schema.Struct({
|
|
29730
|
+
redirectUrl: Schema.String,
|
|
29731
|
+
expiresAt: Schema.Finite
|
|
29732
|
+
});
|
|
29733
|
+
Schema.Literals([
|
|
29734
|
+
"SignedIn",
|
|
29735
|
+
"ProfileUpdated",
|
|
29736
|
+
"Cancelled"
|
|
29737
|
+
]);
|
|
29738
|
+
const sessionRequestsPath = `/_ignotum/v1/session/requests`;
|
|
29739
|
+
const idViewKey = (session) => encodeCanonicalJson([session.user, session.viewRevision]);
|
|
29740
|
+
Schema.Struct({
|
|
29741
|
+
formatVersion: Schema.Literal(1),
|
|
29742
|
+
username: Schema.NullOr(DevelopmentUsername),
|
|
29743
|
+
epoch: Schema.String
|
|
29744
|
+
});
|
|
29745
|
+
const developmentSelectionKey = "ignotum.id.development";
|
|
29746
|
+
//#endregion
|
|
29574
29747
|
//#region ../contracts/dist/runtime/sync.js
|
|
29575
29748
|
const FunctionNamePart = Schema.String.check(Schema.isPattern(/^[A-Za-z_$][A-Za-z0-9_$]*$/));
|
|
29576
29749
|
const ApiFunctionAddressParts = Schema.TemplateLiteralParser([
|
|
@@ -29646,12 +29819,19 @@ const WireFailure = Schema.Struct({
|
|
|
29646
29819
|
});
|
|
29647
29820
|
const WireResult = Schema.Union([WireSuccess, WireFailure]);
|
|
29648
29821
|
const SyncHandshake = Schema.Struct({
|
|
29822
|
+
session: Schema.optional(SessionState),
|
|
29823
|
+
serverTime: Schema.optional(Schema.Finite),
|
|
29649
29824
|
type: Schema.Literal("Handshake"),
|
|
29650
29825
|
appId: AppId,
|
|
29651
29826
|
deploymentId: DeploymentId,
|
|
29652
29827
|
generation: DeploymentGeneration
|
|
29653
29828
|
});
|
|
29654
29829
|
const Snapshot = Schema.Struct({
|
|
29830
|
+
identity: Schema.optional(Schema.Struct({
|
|
29831
|
+
sessionEpoch: Schema.String,
|
|
29832
|
+
viewRevision: Schema.Natural,
|
|
29833
|
+
dependsOnId: Schema.Boolean
|
|
29834
|
+
})),
|
|
29655
29835
|
type: Schema.Literal("Snapshot"),
|
|
29656
29836
|
id: SubscriptionId,
|
|
29657
29837
|
result: WireResult,
|
|
@@ -29662,6 +29842,7 @@ const Snapshot = Schema.Struct({
|
|
|
29662
29842
|
})))
|
|
29663
29843
|
});
|
|
29664
29844
|
const Preparation = Schema.Struct({
|
|
29845
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29665
29846
|
type: Schema.Literal("Preparation"),
|
|
29666
29847
|
id: InvocationId,
|
|
29667
29848
|
kind: Schema.Literal("Mutation"),
|
|
@@ -29671,17 +29852,21 @@ const Preparation = Schema.Struct({
|
|
|
29671
29852
|
}))
|
|
29672
29853
|
});
|
|
29673
29854
|
const SyncResultSuccess = Schema.Struct({
|
|
29855
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29674
29856
|
type: Schema.Literal("Result"),
|
|
29675
29857
|
id: InvocationId,
|
|
29676
29858
|
result: WireSuccess,
|
|
29677
29859
|
committedRevision: AppStateRevision
|
|
29678
29860
|
});
|
|
29679
29861
|
const SyncResultFailure = Schema.Struct({
|
|
29862
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29680
29863
|
type: Schema.Literal("Result"),
|
|
29681
29864
|
id: InvocationId,
|
|
29682
29865
|
result: WireFailure
|
|
29683
29866
|
});
|
|
29684
29867
|
const ErrorCode = Schema.Literals([
|
|
29868
|
+
"SessionChanged",
|
|
29869
|
+
"SessionExpired",
|
|
29685
29870
|
"DuplicateOperationId",
|
|
29686
29871
|
"FunctionUnavailable",
|
|
29687
29872
|
"InvalidArguments",
|
|
@@ -29692,6 +29877,7 @@ const ErrorCode = Schema.Literals([
|
|
|
29692
29877
|
"WrongFunctionKind"
|
|
29693
29878
|
]);
|
|
29694
29879
|
const SyncError = Schema.Struct({
|
|
29880
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29695
29881
|
type: Schema.Literal("Error"),
|
|
29696
29882
|
operation: Schema.optional(Operation),
|
|
29697
29883
|
code: ErrorCode,
|
|
@@ -29702,7 +29888,30 @@ const Deployment = Schema.Struct({
|
|
|
29702
29888
|
deploymentId: DeploymentId,
|
|
29703
29889
|
generation: DeploymentGeneration
|
|
29704
29890
|
});
|
|
29891
|
+
const Session = Schema.Union([Schema.Struct({
|
|
29892
|
+
type: Schema.Literal("Session"),
|
|
29893
|
+
event: Schema.Literal("Updated"),
|
|
29894
|
+
serverTime: Schema.Finite,
|
|
29895
|
+
session: SessionState
|
|
29896
|
+
}), Schema.Struct({
|
|
29897
|
+
type: Schema.Literal("Session"),
|
|
29898
|
+
event: Schema.Literals([
|
|
29899
|
+
"SignedOut",
|
|
29900
|
+
"Revoked",
|
|
29901
|
+
"Expired",
|
|
29902
|
+
"Unavailable",
|
|
29903
|
+
"Replaced"
|
|
29904
|
+
]),
|
|
29905
|
+
sessionEpoch: Schema.String
|
|
29906
|
+
})]);
|
|
29907
|
+
const Invalidate = Schema.Struct({
|
|
29908
|
+
type: Schema.Literal("Invalidate"),
|
|
29909
|
+
sessionEpoch: Schema.String,
|
|
29910
|
+
ids: Schema.Array(SubscriptionId)
|
|
29911
|
+
});
|
|
29705
29912
|
const ServerMessage = Schema.Union([
|
|
29913
|
+
Session,
|
|
29914
|
+
Invalidate,
|
|
29706
29915
|
SyncHandshake,
|
|
29707
29916
|
Snapshot,
|
|
29708
29917
|
Preparation,
|
|
@@ -29736,7 +29945,7 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
|
|
|
29736
29945
|
message: Schema.String
|
|
29737
29946
|
}) {};
|
|
29738
29947
|
//#endregion
|
|
29739
|
-
//#region ../contracts/dist/descriptor-
|
|
29948
|
+
//#region ../contracts/dist/descriptor-Cyo9FP9n.js
|
|
29740
29949
|
const LiteralValue = Schema.Union([
|
|
29741
29950
|
Schema.String,
|
|
29742
29951
|
Schema.Finite,
|
|
@@ -29773,6 +29982,7 @@ const ValueDescriptor = Schema.Union([
|
|
|
29773
29982
|
type: Schema.Literal("id"),
|
|
29774
29983
|
table: Schema.String
|
|
29775
29984
|
}),
|
|
29985
|
+
Schema.Struct({ type: Schema.Literal("userId") }),
|
|
29776
29986
|
Schema.Struct({ type: Schema.Literal("integer") }),
|
|
29777
29987
|
Schema.Struct({
|
|
29778
29988
|
type: Schema.Literal("literal"),
|
|
@@ -29843,6 +30053,7 @@ const descriptorAllowedInEnvironment = (descriptor) => {
|
|
|
29843
30053
|
switch (descriptor.type) {
|
|
29844
30054
|
case "error":
|
|
29845
30055
|
case "file":
|
|
30056
|
+
case "userId":
|
|
29846
30057
|
case "id": return false;
|
|
29847
30058
|
case "array":
|
|
29848
30059
|
case "nullable":
|
|
@@ -29869,22 +30080,6 @@ const descriptorFields = (fields) => {
|
|
|
29869
30080
|
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
29870
30081
|
};
|
|
29871
30082
|
//#endregion
|
|
29872
|
-
//#region ../contracts/dist/versioned.js
|
|
29873
|
-
/** Registers the first revision of a long-lived format. */
|
|
29874
|
-
const initial = (schema) => schema;
|
|
29875
|
-
/** Adds one adjacent migration and keeps encoding on the new current revision. */
|
|
29876
|
-
const upgrade = (previous, current, migrate) => {
|
|
29877
|
-
const accepted = Schema.Union([previous, current]);
|
|
29878
|
-
const isCurrent = Schema.is(current);
|
|
29879
|
-
return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
|
|
29880
|
-
decode: SchemaGetter.transform((value) => {
|
|
29881
|
-
if (isCurrent(value)) return value;
|
|
29882
|
-
return migrate(value);
|
|
29883
|
-
}),
|
|
29884
|
-
encode: SchemaGetter.transform((value) => value)
|
|
29885
|
-
}));
|
|
29886
|
-
};
|
|
29887
|
-
//#endregion
|
|
29888
30083
|
//#region ../contracts/dist/deployment.js
|
|
29889
30084
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
29890
30085
|
const ClientPath = Schema.String.check(Schema.isPattern(/^\/(?!_ignotum(?:\/|$))(?:(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+\/?)?$/)).pipe(Schema.brand("ignotum/deployment/ClientPath"));
|
|
@@ -30140,7 +30335,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
30140
30335
|
};
|
|
30141
30336
|
//#endregion
|
|
30142
30337
|
//#region package.json
|
|
30143
|
-
var version = "0.0.
|
|
30338
|
+
var version = "0.0.13";
|
|
30144
30339
|
//#endregion
|
|
30145
30340
|
//#region src/cli/codegen.ts
|
|
30146
30341
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30578,92 +30773,6 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
|
30578
30773
|
});
|
|
30579
30774
|
}));
|
|
30580
30775
|
//#endregion
|
|
30581
|
-
//#region ../contracts/dist/json.js
|
|
30582
|
-
const encodeJson = (value) => {
|
|
30583
|
-
if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
|
|
30584
|
-
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
|
|
30585
|
-
return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String$1.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
|
|
30586
|
-
};
|
|
30587
|
-
const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
|
|
30588
|
-
//#endregion
|
|
30589
|
-
//#region ../contracts/dist/runtime/result.js
|
|
30590
|
-
const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
|
|
30591
|
-
const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
|
|
30592
|
-
const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
|
|
30593
|
-
const ErrorBrand = "ignotum/error";
|
|
30594
|
-
Brand.nominal();
|
|
30595
|
-
const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
|
|
30596
|
-
var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: RequestId }) {};
|
|
30597
|
-
const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
|
|
30598
|
-
const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
|
|
30599
|
-
function makeResultEffectBase() {
|
|
30600
|
-
const Base = function() {};
|
|
30601
|
-
Base.prototype = Effectable.Prototype({
|
|
30602
|
-
label: "IgnotumResult",
|
|
30603
|
-
evaluate() {
|
|
30604
|
-
return this[ResultTypeId];
|
|
30605
|
-
}
|
|
30606
|
-
});
|
|
30607
|
-
return Base;
|
|
30608
|
-
}
|
|
30609
|
-
const ResultEffectBase = makeResultEffectBase();
|
|
30610
|
-
var ResultOperation = class ResultOperation extends ResultEffectBase {
|
|
30611
|
-
[ResultTypeId];
|
|
30612
|
-
constructor(effect, completed) {
|
|
30613
|
-
super();
|
|
30614
|
-
this[ResultTypeId] = effect;
|
|
30615
|
-
if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
|
|
30616
|
-
}
|
|
30617
|
-
catch(cases) {
|
|
30618
|
-
const effectCases = {};
|
|
30619
|
-
for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
|
|
30620
|
-
const runtimeEffect = this[ResultTypeId];
|
|
30621
|
-
const caught = Effect.catchTags(runtimeEffect, effectCases);
|
|
30622
|
-
return new ResultOperation(caught);
|
|
30623
|
-
}
|
|
30624
|
-
};
|
|
30625
|
-
var PendingQueryResult = class {
|
|
30626
|
-
[PendingResultTypeId] = PendingResultTypeId;
|
|
30627
|
-
};
|
|
30628
|
-
const inspectResult = (value) => {
|
|
30629
|
-
if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
|
|
30630
|
-
return value[CompletedResultTypeId];
|
|
30631
|
-
};
|
|
30632
|
-
const resultFromEffect = (effect) => new ResultOperation(effect);
|
|
30633
|
-
const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
|
|
30634
|
-
Function$1.dual(2, (result, matchers) => {
|
|
30635
|
-
if (result instanceof PendingQueryResult) {
|
|
30636
|
-
if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
|
|
30637
|
-
return matchers.pending();
|
|
30638
|
-
}
|
|
30639
|
-
if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
|
|
30640
|
-
const inspected = inspectResult(result);
|
|
30641
|
-
if (inspected?.type === "Success") {
|
|
30642
|
-
const value = inspected.value;
|
|
30643
|
-
return matchers.value(value);
|
|
30644
|
-
}
|
|
30645
|
-
if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
|
|
30646
|
-
const error = result[CompletedResultTypeId].error;
|
|
30647
|
-
if (error._tag === "InternalServerError") {
|
|
30648
|
-
const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
|
|
30649
|
-
const handler = matchers.internalError;
|
|
30650
|
-
if (handler === void 0) throw internalError;
|
|
30651
|
-
return handler(internalError);
|
|
30652
|
-
}
|
|
30653
|
-
const errorMatcher = matchers.error;
|
|
30654
|
-
if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
30655
|
-
if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
|
|
30656
|
-
const handlers = errorMatcher;
|
|
30657
|
-
const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
|
|
30658
|
-
if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
30659
|
-
return handler(error);
|
|
30660
|
-
});
|
|
30661
|
-
const documentNotFound = (table, id) => Brand.nominal()({
|
|
30662
|
-
_tag: "DocumentNotFound",
|
|
30663
|
-
table,
|
|
30664
|
-
id
|
|
30665
|
-
});
|
|
30666
|
-
//#endregion
|
|
30667
30776
|
//#region ../contracts/dist/runtime/pagination.js
|
|
30668
30777
|
const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
|
|
30669
30778
|
const PaginationPageSize = Schema.Int.check(Schema.isBetween({
|
|
@@ -31509,19 +31618,19 @@ const resolvedClientEntryId = `\0${clientEntryId}`;
|
|
|
31509
31618
|
const clientStylesId = "virtual:ignotum/_styles.css";
|
|
31510
31619
|
const resolvedClientStylesId = `\0${clientStylesId}`;
|
|
31511
31620
|
const clientStylesSource = "@import \"tailwindcss\";\n";
|
|
31512
|
-
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
31621
|
+
const escapeHtml$1 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
31513
31622
|
const renderClientDocument = (options) => `<!doctype html>
|
|
31514
31623
|
<html lang="en">
|
|
31515
31624
|
<head>
|
|
31516
31625
|
<meta charset="UTF-8" />
|
|
31517
31626
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
31518
|
-
<title>${escapeHtml(options.title)}</title>
|
|
31519
|
-
${options.icon === void 0 ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml(options.icon)}" />`}
|
|
31520
|
-
${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml(href)}" />`).join("\n")}
|
|
31627
|
+
<title>${escapeHtml$1(options.title)}</title>
|
|
31628
|
+
${options.icon === void 0 ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml$1(options.icon)}" />`}
|
|
31629
|
+
${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml$1(href)}" />`).join("\n")}
|
|
31521
31630
|
</head>
|
|
31522
31631
|
<body>
|
|
31523
31632
|
<div id="app"></div>
|
|
31524
|
-
${options.scripts.map((script) => script.type === "External" ? ` <script type="module" crossorigin src="/${escapeHtml(script.source)}"><\/script>` : ` <script type="module">\n ${script.source}\n <\/script>`).join("\n")}
|
|
31633
|
+
${options.scripts.map((script) => script.type === "External" ? ` <script type="module" crossorigin src="/${escapeHtml$1(script.source)}"><\/script>` : ` <script type="module">\n ${script.source}\n <\/script>`).join("\n")}
|
|
31525
31634
|
</body>
|
|
31526
31635
|
</html>`;
|
|
31527
31636
|
const renderClientEntry = (development) => `${development ? "import \"preact/debug\"\n" : ""}import { render } from "preact"
|
|
@@ -32681,195 +32790,6 @@ const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (appDirectory)
|
|
|
32681
32790
|
}));
|
|
32682
32791
|
});
|
|
32683
32792
|
//#endregion
|
|
32684
|
-
//#region ../../node_modules/.pnpm/@effect+sql-sqlite-node@4.0.0-rc.111_effect@4.0.0-rc.111/node_modules/@effect/sql-sqlite-node/dist/SqliteClient.js
|
|
32685
|
-
/**
|
|
32686
|
-
* Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
|
|
32687
|
-
*
|
|
32688
|
-
* This module opens a SQLite database and exposes it as both `SqliteClient` and
|
|
32689
|
-
* the generic Effect SQL client. It serializes access through one connection,
|
|
32690
|
-
* caches prepared statements, enables WAL mode unless disabled, and waits up
|
|
32691
|
-
* to five seconds for busy databases by default. Explicit transactions on
|
|
32692
|
-
* writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock
|
|
32693
|
-
* upgrades, which serializes them behind other writers even when they only
|
|
32694
|
-
* read. Clients opened with `readonly: true` are unaffected. Busy waits block
|
|
32695
|
-
* the Node.js event loop because `node:sqlite` is synchronous. Database backup
|
|
32696
|
-
* and extension loading are supported; streaming queries and `updateValues`
|
|
32697
|
-
* are not.
|
|
32698
|
-
*
|
|
32699
|
-
* @since 4.0.0
|
|
32700
|
-
*/
|
|
32701
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
32702
|
-
const MAX_BUSY_TIMEOUT = 2147483647;
|
|
32703
|
-
/**
|
|
32704
|
-
* Runtime type identifier used to mark Node `SqliteClient` values.
|
|
32705
|
-
*
|
|
32706
|
-
* @category type IDs
|
|
32707
|
-
* @since 4.0.0
|
|
32708
|
-
*/
|
|
32709
|
-
const TypeId$1 = "~@effect/sql-sqlite-node/SqliteClient";
|
|
32710
|
-
/**
|
|
32711
|
-
* Service tag for the node SQLite client implementation.
|
|
32712
|
-
*
|
|
32713
|
-
* @category services
|
|
32714
|
-
* @since 4.0.0
|
|
32715
|
-
*/
|
|
32716
|
-
const SqliteClient = /*#__PURE__*/ Context$1.Service("@effect/sql-sqlite-node/SqliteClient");
|
|
32717
|
-
/**
|
|
32718
|
-
* Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL and a 5-second busy timeout enabled by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected.
|
|
32719
|
-
*
|
|
32720
|
-
* @category constructors
|
|
32721
|
-
* @since 4.0.0
|
|
32722
|
-
*/
|
|
32723
|
-
const make$2 = (options) => Effect$1.gen(function* () {
|
|
32724
|
-
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
|
|
32725
|
-
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : void 0;
|
|
32726
|
-
const makeConnection = Effect$1.gen(function* () {
|
|
32727
|
-
const scope = yield* Effect$1.scope;
|
|
32728
|
-
const db = new DatabaseSync(options.filename, {
|
|
32729
|
-
readOnly: options.readonly ?? false,
|
|
32730
|
-
allowExtension: true
|
|
32731
|
-
});
|
|
32732
|
-
yield* Scope.addFinalizer(scope, Effect$1.sync(() => db.close()));
|
|
32733
|
-
db.enableLoadExtension(false);
|
|
32734
|
-
const busyTimeout = Math.min(MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))));
|
|
32735
|
-
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
32736
|
-
if (options.disableWAL !== true) db.exec("PRAGMA journal_mode = WAL");
|
|
32737
|
-
const prepareCache = yield* Cache.make({
|
|
32738
|
-
capacity: options.prepareCacheSize ?? 200,
|
|
32739
|
-
timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
|
|
32740
|
-
lookup: (sql) => Effect$1.try({
|
|
32741
|
-
try: () => db.prepare(sql),
|
|
32742
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
|
|
32743
|
-
})
|
|
32744
|
-
});
|
|
32745
|
-
const runStatement = (statement, params, raw) => Effect$1.withFiber((fiber) => {
|
|
32746
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32747
|
-
return Effect$1.try({
|
|
32748
|
-
try: () => {
|
|
32749
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32750
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32751
|
-
const result = statement.run(...params);
|
|
32752
|
-
return raw ? {
|
|
32753
|
-
changes: result.changes,
|
|
32754
|
-
lastInsertRowid: result.lastInsertRowid
|
|
32755
|
-
} : [];
|
|
32756
|
-
},
|
|
32757
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32758
|
-
});
|
|
32759
|
-
});
|
|
32760
|
-
const runStatementValues = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
32761
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32762
|
-
return Effect$1.try({
|
|
32763
|
-
try: () => {
|
|
32764
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32765
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32766
|
-
statement.run(...params);
|
|
32767
|
-
return [];
|
|
32768
|
-
},
|
|
32769
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32770
|
-
});
|
|
32771
|
-
});
|
|
32772
|
-
const runStatementValuesUnprepared = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
32773
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32774
|
-
return Effect$1.try({
|
|
32775
|
-
try: () => {
|
|
32776
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32777
|
-
statement.setReturnArrays(true);
|
|
32778
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32779
|
-
statement.run(...params);
|
|
32780
|
-
return [];
|
|
32781
|
-
},
|
|
32782
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32783
|
-
});
|
|
32784
|
-
});
|
|
32785
|
-
const run = (sql, params, raw = false) => Effect$1.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
|
|
32786
|
-
const runValues = (sql, params) => Effect$1.acquireUseRelease(Cache.get(prepareCache, sql), (statement) => {
|
|
32787
|
-
statement.setReturnArrays(true);
|
|
32788
|
-
return runStatementValues(statement, params);
|
|
32789
|
-
}, (statement) => Effect$1.sync(() => statement.setReturnArrays(false)));
|
|
32790
|
-
const runValuesUnprepared = (sql, params) => runStatementValuesUnprepared(db.prepare(sql), params);
|
|
32791
|
-
return identity({
|
|
32792
|
-
execute(sql, params, transformRows) {
|
|
32793
|
-
return transformRows ? Effect$1.map(run(sql, params), transformRows) : run(sql, params);
|
|
32794
|
-
},
|
|
32795
|
-
executeRaw(sql, params) {
|
|
32796
|
-
return run(sql, params, true);
|
|
32797
|
-
},
|
|
32798
|
-
executeValues(sql, params) {
|
|
32799
|
-
return runValues(sql, params);
|
|
32800
|
-
},
|
|
32801
|
-
executeValuesUnprepared(sql, params) {
|
|
32802
|
-
return runValuesUnprepared(sql, params);
|
|
32803
|
-
},
|
|
32804
|
-
executeUnprepared(sql, params, transformRows) {
|
|
32805
|
-
const effect = runStatement(db.prepare(sql), params ?? [], false);
|
|
32806
|
-
return transformRows ? Effect$1.map(effect, transformRows) : effect;
|
|
32807
|
-
},
|
|
32808
|
-
executeStream(_sql, _params) {
|
|
32809
|
-
return Stream$1.die("executeStream not implemented");
|
|
32810
|
-
},
|
|
32811
|
-
backup(destination) {
|
|
32812
|
-
return Effect$1.suspend(() => {
|
|
32813
|
-
let totalPages = 0;
|
|
32814
|
-
return Effect$1.tryPromise({
|
|
32815
|
-
try: () => backup(db, destination, { progress: (progress) => {
|
|
32816
|
-
totalPages = progress.totalPages;
|
|
32817
|
-
} }).then((pages) => ({
|
|
32818
|
-
totalPages: totalPages || pages,
|
|
32819
|
-
remainingPages: 0
|
|
32820
|
-
})),
|
|
32821
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
|
|
32822
|
-
});
|
|
32823
|
-
});
|
|
32824
|
-
},
|
|
32825
|
-
loadExtension(path) {
|
|
32826
|
-
return Effect$1.acquireUseRelease(Effect$1.sync(() => db.enableLoadExtension(true)), () => Effect$1.try({
|
|
32827
|
-
try: () => db.loadExtension(path),
|
|
32828
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
|
|
32829
|
-
}), () => Effect$1.sync(() => db.enableLoadExtension(false)));
|
|
32830
|
-
}
|
|
32831
|
-
});
|
|
32832
|
-
});
|
|
32833
|
-
const semaphore = yield* Semaphore$1.make(1);
|
|
32834
|
-
const connection = yield* makeConnection;
|
|
32835
|
-
const acquirer = semaphore.withPermits(1)(Effect$1.succeed(connection));
|
|
32836
|
-
const transactionAcquirer = Effect$1.uninterruptibleMask((restore) => {
|
|
32837
|
-
const fiber = Fiber.getCurrent();
|
|
32838
|
-
const scope = Context$1.getUnsafe(fiber.context, Scope.Scope);
|
|
32839
|
-
return Effect$1.as(Effect$1.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection);
|
|
32840
|
-
});
|
|
32841
|
-
return Object.assign(yield* Client.make({
|
|
32842
|
-
acquirer,
|
|
32843
|
-
compiler,
|
|
32844
|
-
transactionAcquirer,
|
|
32845
|
-
beginTransaction: "BEGIN IMMEDIATE",
|
|
32846
|
-
spanAttributes: [...options.spanAttributes ? Object.entries(options.spanAttributes) : [], [ATTR_DB_SYSTEM_NAME, "sqlite"]],
|
|
32847
|
-
transformRows
|
|
32848
|
-
}), {
|
|
32849
|
-
[TypeId$1]: TypeId$1,
|
|
32850
|
-
config: options,
|
|
32851
|
-
backup: (destination) => Effect$1.flatMap(acquirer, (_) => _.backup(destination)),
|
|
32852
|
-
loadExtension: (path) => Effect$1.flatMap(acquirer, (_) => _.loadExtension(path))
|
|
32853
|
-
});
|
|
32854
|
-
});
|
|
32855
|
-
/**
|
|
32856
|
-
* Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
|
|
32857
|
-
*
|
|
32858
|
-
* @category layers
|
|
32859
|
-
* @since 4.0.0
|
|
32860
|
-
*/
|
|
32861
|
-
const layer = (config) => Layer$1.effectContext(Effect$1.map(make$2(config), (client) => Context$1.make(SqliteClient, client).pipe(Context$1.add(Client.SqlClient, client)))).pipe(Layer$1.provide(Reactivity.layer));
|
|
32862
|
-
const classifyError = (cause, message, operation) => classifySqliteError(sqliteCauseWithErrno(cause), {
|
|
32863
|
-
message,
|
|
32864
|
-
operation
|
|
32865
|
-
});
|
|
32866
|
-
const sqliteCauseWithErrno = (cause) => {
|
|
32867
|
-
if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) return cause;
|
|
32868
|
-
const errcode = cause.errcode;
|
|
32869
|
-
if (typeof errcode !== "number") return cause;
|
|
32870
|
-
return Object.assign(cause, { errno: errcode });
|
|
32871
|
-
};
|
|
32872
|
-
//#endregion
|
|
32873
32793
|
//#region ../contracts/dist/runtime/index.js
|
|
32874
32794
|
const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
|
|
32875
32795
|
const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
|
|
@@ -32903,6 +32823,7 @@ const scalarKind = (descriptor) => {
|
|
|
32903
32823
|
case "date":
|
|
32904
32824
|
case "integer":
|
|
32905
32825
|
case "number": return "number";
|
|
32826
|
+
case "userId":
|
|
32906
32827
|
case "id":
|
|
32907
32828
|
case "string": return "string";
|
|
32908
32829
|
case "literal": return scalarValueKind(descriptor.value);
|
|
@@ -33001,7 +32922,9 @@ const IndexPointInvalidation = Schema.Struct({
|
|
|
33001
32922
|
index: IndexIdentity,
|
|
33002
32923
|
key: EncodedIndexKey
|
|
33003
32924
|
});
|
|
32925
|
+
const IdDependency = Schema.Struct({ type: Schema.Literal("Id") });
|
|
33004
32926
|
const ReadDependency = Schema.Union([
|
|
32927
|
+
IdDependency,
|
|
33005
32928
|
TableDependency,
|
|
33006
32929
|
DocumentDependency,
|
|
33007
32930
|
IndexRangeDependency
|
|
@@ -33066,7 +32989,8 @@ const InvocationBase = {
|
|
|
33066
32989
|
generation: DeploymentGeneration,
|
|
33067
32990
|
invocationKey: InvocationKey,
|
|
33068
32991
|
function: FunctionAddress,
|
|
33069
|
-
args: Schema.Json
|
|
32992
|
+
args: Schema.Json,
|
|
32993
|
+
identity: Schema.optional(SessionState)
|
|
33070
32994
|
};
|
|
33071
32995
|
const QueryInvocation = Schema.Struct({
|
|
33072
32996
|
...InvocationBase,
|
|
@@ -33089,6 +33013,7 @@ var RuntimeDeploymentUnavailable = class extends Schema.TaggedError()("RuntimeDe
|
|
|
33089
33013
|
message: Schema.String
|
|
33090
33014
|
}) {};
|
|
33091
33015
|
const RuntimeInvocationRejectionCode = Schema.Literals([
|
|
33016
|
+
"SessionExpired",
|
|
33092
33017
|
"FunctionUnavailable",
|
|
33093
33018
|
"InvalidArguments",
|
|
33094
33019
|
"InvocationIdConflict",
|
|
@@ -33114,7 +33039,7 @@ Schema.Union([
|
|
|
33114
33039
|
RuntimeInvocationRejected,
|
|
33115
33040
|
RuntimeInvocationUnavailable
|
|
33116
33041
|
]);
|
|
33117
|
-
const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
|
|
33042
|
+
const dependencyKey = (dependency) => dependency.type === "Id" ? "Id" : dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
|
|
33118
33043
|
dependency.type,
|
|
33119
33044
|
dependency.tableId,
|
|
33120
33045
|
dependency.id
|
|
@@ -33133,11 +33058,17 @@ const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make
|
|
|
33133
33058
|
functionAddress,
|
|
33134
33059
|
args
|
|
33135
33060
|
]));
|
|
33136
|
-
const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args) => encodeCanonicalJson([
|
|
33061
|
+
const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args, sessionEpoch) => encodeCanonicalJson(sessionEpoch === void 0 ? [
|
|
33137
33062
|
deploymentId,
|
|
33138
33063
|
functionKind,
|
|
33139
33064
|
functionAddress,
|
|
33140
33065
|
args
|
|
33066
|
+
] : [
|
|
33067
|
+
deploymentId,
|
|
33068
|
+
functionKind,
|
|
33069
|
+
functionAddress,
|
|
33070
|
+
args,
|
|
33071
|
+
sessionEpoch
|
|
33141
33072
|
]);
|
|
33142
33073
|
const tableDependency = (tableId) => ({
|
|
33143
33074
|
type: "Table",
|
|
@@ -33162,68 +33093,6 @@ const indexPointInvalidation = (tableId, index, key) => ({
|
|
|
33162
33093
|
key
|
|
33163
33094
|
});
|
|
33164
33095
|
//#endregion
|
|
33165
|
-
//#region ../runtime/dist/functions.js
|
|
33166
|
-
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
33167
|
-
//#endregion
|
|
33168
|
-
//#region ../contracts/dist/runtime/transport.js
|
|
33169
|
-
const runtimeInvocationPath = "/v1/invoke";
|
|
33170
|
-
const runtimeRevisionPath = "/v1/revision";
|
|
33171
|
-
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
33172
|
-
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
33173
|
-
const runtimeFileEffectsPath = "/v1/files/effects";
|
|
33174
|
-
const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
|
|
33175
|
-
const ignotumPathPrefix$1 = "/_ignotum";
|
|
33176
|
-
const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
|
|
33177
|
-
const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
|
|
33178
|
-
const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
|
|
33179
|
-
const RuntimeRequestPath = Schema.Literals([
|
|
33180
|
-
runtimeInvocationPath,
|
|
33181
|
-
runtimeRevisionPath,
|
|
33182
|
-
runtimeIndexPreparePath,
|
|
33183
|
-
runtimeIndexCommitPath,
|
|
33184
|
-
runtimeFileEffectsPath,
|
|
33185
|
-
runtimeFileEffectsAcknowledgePath
|
|
33186
|
-
]);
|
|
33187
|
-
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
33188
|
-
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
33189
|
-
Schema.Struct({
|
|
33190
|
-
appId: AppId,
|
|
33191
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
33192
|
-
nonce: RuntimeRequestNonce,
|
|
33193
|
-
timestamp: RuntimeRequestTimestamp,
|
|
33194
|
-
signature: RuntimeRequestSignature
|
|
33195
|
-
});
|
|
33196
|
-
Schema.Struct({
|
|
33197
|
-
method: Schema.Literal("POST"),
|
|
33198
|
-
path: RuntimeRequestPath,
|
|
33199
|
-
timestamp: RuntimeRequestTimestamp,
|
|
33200
|
-
nonce: RuntimeRequestNonce,
|
|
33201
|
-
appId: AppId,
|
|
33202
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
33203
|
-
bodySha256: Sha256
|
|
33204
|
-
});
|
|
33205
|
-
Schema.Struct({
|
|
33206
|
-
appId: AppId,
|
|
33207
|
-
deploymentId: DeploymentId
|
|
33208
|
-
});
|
|
33209
|
-
Schema.Struct({ completed: Schema.Literal(true) });
|
|
33210
|
-
const RuntimeErrorCode = Schema.Literals([
|
|
33211
|
-
"InvalidRequest",
|
|
33212
|
-
"InvalidSignature",
|
|
33213
|
-
"RequestExpired",
|
|
33214
|
-
"RequestReplay",
|
|
33215
|
-
"InvocationRejected",
|
|
33216
|
-
"RuntimeUnavailable"
|
|
33217
|
-
]);
|
|
33218
|
-
Schema.Struct({
|
|
33219
|
-
code: RuntimeErrorCode,
|
|
33220
|
-
message: Schema.String,
|
|
33221
|
-
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
33222
|
-
});
|
|
33223
|
-
//#endregion
|
|
33224
|
-
//#region ../shared/dist/http-body.js
|
|
33225
|
-
const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => stream.pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.mkUint8Array));
|
|
33226
|
-
//#endregion
|
|
33227
33096
|
//#region ../runtime/dist/sync.js
|
|
33228
33097
|
const makeDependencyIndex = () => {
|
|
33229
33098
|
const dependenciesByQuery = MutableHashMap.empty();
|
|
@@ -33294,6 +33163,462 @@ const makeDependencyIndex = () => {
|
|
|
33294
33163
|
};
|
|
33295
33164
|
};
|
|
33296
33165
|
var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
|
|
33166
|
+
//#endregion
|
|
33167
|
+
//#region src/dev-runtime/query-cache.ts
|
|
33168
|
+
var LocalQueryCache = class LocalQueryCache extends Context.Service()("ignotum/dev-runtime/query-cache/LocalQueryCache") {
|
|
33169
|
+
static layer = Layer.effect(LocalQueryCache, Effect.sync(() => {
|
|
33170
|
+
const results = /* @__PURE__ */ new Map();
|
|
33171
|
+
const dependencies = makeDependencyIndex();
|
|
33172
|
+
const flights = /* @__PURE__ */ new Map();
|
|
33173
|
+
const publicQueries = /* @__PURE__ */ new Set();
|
|
33174
|
+
const locks = /* @__PURE__ */ new Map();
|
|
33175
|
+
let generation = 0;
|
|
33176
|
+
const save = (key, result) => {
|
|
33177
|
+
results.delete(key);
|
|
33178
|
+
results.set(key, result);
|
|
33179
|
+
dependencies.record(key, result.dependencies);
|
|
33180
|
+
while (results.size > 128) {
|
|
33181
|
+
const oldest = results.keys().next().value;
|
|
33182
|
+
if (oldest === void 0) break;
|
|
33183
|
+
results.delete(oldest);
|
|
33184
|
+
dependencies.remove(oldest);
|
|
33185
|
+
publicQueries.delete(oldest);
|
|
33186
|
+
}
|
|
33187
|
+
};
|
|
33188
|
+
return LocalQueryCache.of({
|
|
33189
|
+
invalidate: (event) => {
|
|
33190
|
+
generation++;
|
|
33191
|
+
const keys = event.type === "All" ? [...results.keys()] : [...dependencies.affected(event.invalidations)];
|
|
33192
|
+
for (const key of keys) {
|
|
33193
|
+
results.delete(key);
|
|
33194
|
+
dependencies.remove(key);
|
|
33195
|
+
}
|
|
33196
|
+
if (event.type === "All") publicQueries.clear();
|
|
33197
|
+
},
|
|
33198
|
+
execute: Effect.fn("LocalQueryCache.execute")(function* (base, identity, minimumRevision, execute) {
|
|
33199
|
+
const admitted = generation;
|
|
33200
|
+
const scoped = `${base}:id:${idViewKey(identity)}`;
|
|
33201
|
+
const cached = results.get(base) ?? results.get(scoped);
|
|
33202
|
+
if (cached !== void 0 && cached.observedRevision >= minimumRevision) return cached;
|
|
33203
|
+
const key = `${scoped}:${admitted}`;
|
|
33204
|
+
const pending = flights.get(key);
|
|
33205
|
+
if (pending !== void 0) return yield* Deferred.await(pending);
|
|
33206
|
+
const done = yield* Deferred.make();
|
|
33207
|
+
flights.set(key, done);
|
|
33208
|
+
const evaluate = Effect.gen(function* () {
|
|
33209
|
+
const shared = results.get(base);
|
|
33210
|
+
if (shared !== void 0 && shared.observedRevision >= minimumRevision) return shared;
|
|
33211
|
+
const result = yield* execute;
|
|
33212
|
+
if (result.type === "Query" && generation === admitted) {
|
|
33213
|
+
const readsId = result.dependencies.some((dependency) => dependency.type === "Id");
|
|
33214
|
+
save(scoped, result);
|
|
33215
|
+
if (!readsId) {
|
|
33216
|
+
publicQueries.add(base);
|
|
33217
|
+
save(base, result);
|
|
33218
|
+
}
|
|
33219
|
+
}
|
|
33220
|
+
return result;
|
|
33221
|
+
});
|
|
33222
|
+
const lockKey = `${base}:${admitted}`;
|
|
33223
|
+
let lock = locks.get(lockKey);
|
|
33224
|
+
if (publicQueries.has(base) && lock === void 0) {
|
|
33225
|
+
lock = Semaphore.makeUnsafe(1);
|
|
33226
|
+
locks.set(lockKey, lock);
|
|
33227
|
+
}
|
|
33228
|
+
return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
33229
|
+
const exit = yield* restore(lock === void 0 ? evaluate : lock.withPermits(1)(evaluate)).pipe(Effect.exit);
|
|
33230
|
+
Deferred.doneUnsafe(done, Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause));
|
|
33231
|
+
flights.delete(key);
|
|
33232
|
+
locks.delete(lockKey);
|
|
33233
|
+
return yield* exit;
|
|
33234
|
+
}));
|
|
33235
|
+
})
|
|
33236
|
+
});
|
|
33237
|
+
}));
|
|
33238
|
+
};
|
|
33239
|
+
//#endregion
|
|
33240
|
+
//#region src/dev-runtime/id.ts
|
|
33241
|
+
const Profile = Schema.Struct({
|
|
33242
|
+
user: User,
|
|
33243
|
+
share: ProfileFields,
|
|
33244
|
+
revision: Schema.Natural
|
|
33245
|
+
});
|
|
33246
|
+
const Profiles = initial(Schema.Struct({
|
|
33247
|
+
formatVersion: Schema.Literal(1),
|
|
33248
|
+
profiles: Schema.Record(Schema.String, Profile)
|
|
33249
|
+
}));
|
|
33250
|
+
const ProfilesJson = Schema.fromJsonString(Profiles);
|
|
33251
|
+
const localIdPath = "/_ignotum/v1/id";
|
|
33252
|
+
const LocalCompletion = Schema.Struct({
|
|
33253
|
+
request: SessionRequest,
|
|
33254
|
+
username: DevelopmentUsername,
|
|
33255
|
+
name: Schema.String,
|
|
33256
|
+
email: Schema.String,
|
|
33257
|
+
image: Schema.String,
|
|
33258
|
+
share: ProfileFields,
|
|
33259
|
+
manage: Schema.optional(Schema.Boolean)
|
|
33260
|
+
});
|
|
33261
|
+
const projection = (user, share) => ({
|
|
33262
|
+
id: user.id,
|
|
33263
|
+
name: share.includes("name") ? user.name : void 0,
|
|
33264
|
+
email: share.includes("email") ? user.email : void 0,
|
|
33265
|
+
image: share.includes("image") ? user.image : void 0
|
|
33266
|
+
});
|
|
33267
|
+
var LocalId = class LocalId extends Context.Service()("ignotum/dev-runtime/id/LocalId") {
|
|
33268
|
+
static layer = (directory) => Layer.effect(LocalId, Effect.gen(function* () {
|
|
33269
|
+
const fs = yield* FileSystem.FileSystem;
|
|
33270
|
+
const path = yield* Path$1.Path;
|
|
33271
|
+
const file = path.join(directory, ".ignotum", "id.json");
|
|
33272
|
+
const lock = yield* Semaphore.make(1);
|
|
33273
|
+
const load = Effect.fn("LocalId.load")(function* () {
|
|
33274
|
+
if (!(yield* fs.exists(file))) return Profiles.make({
|
|
33275
|
+
formatVersion: 1,
|
|
33276
|
+
profiles: {}
|
|
33277
|
+
});
|
|
33278
|
+
return yield* fs.readFileString(file).pipe(Effect.flatMap(Schema.decodeEffect(ProfilesJson)));
|
|
33279
|
+
});
|
|
33280
|
+
return LocalId.of({
|
|
33281
|
+
profile: Effect.fn("LocalId.profile")(function* (username) {
|
|
33282
|
+
return (yield* load().pipe(Effect.orDie)).profiles[username];
|
|
33283
|
+
}),
|
|
33284
|
+
session: Effect.fn("LocalId.session")(function* (username, epoch) {
|
|
33285
|
+
if (username === null) return {
|
|
33286
|
+
sessionEpoch: epoch,
|
|
33287
|
+
viewRevision: 0,
|
|
33288
|
+
user: null,
|
|
33289
|
+
validUntil: Number.MAX_SAFE_INTEGER
|
|
33290
|
+
};
|
|
33291
|
+
const id = yield* Schema.decodeEffect(UserId)(username).pipe(Effect.orDie);
|
|
33292
|
+
const profile = (yield* load().pipe(Effect.orDie)).profiles[id];
|
|
33293
|
+
const user = projection(profile?.user ?? { id }, profile?.share ?? []);
|
|
33294
|
+
return {
|
|
33295
|
+
sessionEpoch: epoch,
|
|
33296
|
+
viewRevision: profile?.revision ?? 0,
|
|
33297
|
+
user,
|
|
33298
|
+
validUntil: Number.MAX_SAFE_INTEGER
|
|
33299
|
+
};
|
|
33300
|
+
}),
|
|
33301
|
+
complete: Effect.fn("LocalId.complete")(function* (input) {
|
|
33302
|
+
yield* lock.withPermits(1)(Effect.gen(function* () {
|
|
33303
|
+
const stored = yield* load();
|
|
33304
|
+
const previous = stored.profiles[input.username];
|
|
33305
|
+
const user = User.make({
|
|
33306
|
+
id: UserId.make(input.username),
|
|
33307
|
+
name: input.name === "" ? void 0 : input.name,
|
|
33308
|
+
email: input.email === "" ? void 0 : input.email,
|
|
33309
|
+
image: input.image === "" ? void 0 : input.image
|
|
33310
|
+
});
|
|
33311
|
+
const share = input.manage === true ? input.share : [.../* @__PURE__ */ new Set([...previous?.share ?? [], ...input.share.filter((field) => input.request.profile.includes(field))])];
|
|
33312
|
+
const updated = Profiles.make({
|
|
33313
|
+
formatVersion: 1,
|
|
33314
|
+
profiles: {
|
|
33315
|
+
...stored.profiles,
|
|
33316
|
+
[input.username]: {
|
|
33317
|
+
user,
|
|
33318
|
+
share,
|
|
33319
|
+
revision: (previous?.revision ?? 0) + Number(JSON.stringify(projection(previous?.user ?? { id: user.id }, previous?.share ?? [])) !== JSON.stringify(projection(user, share)))
|
|
33320
|
+
}
|
|
33321
|
+
}
|
|
33322
|
+
});
|
|
33323
|
+
yield* fs.makeDirectory(path.dirname(file), { recursive: true });
|
|
33324
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
33325
|
+
const temporary = yield* fs.makeTempFileScoped({
|
|
33326
|
+
directory: path.dirname(file),
|
|
33327
|
+
prefix: ".id-"
|
|
33328
|
+
});
|
|
33329
|
+
yield* fs.writeFileString(temporary, yield* Schema.encodeEffect(ProfilesJson)(updated));
|
|
33330
|
+
yield* fs.rename(temporary, file);
|
|
33331
|
+
}));
|
|
33332
|
+
})).pipe(Effect.orDie);
|
|
33333
|
+
})
|
|
33334
|
+
});
|
|
33335
|
+
}));
|
|
33336
|
+
};
|
|
33337
|
+
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll("\"", """);
|
|
33338
|
+
const localIdPage = (request) => {
|
|
33339
|
+
const encoded = JSON.stringify(request).replaceAll("<", "\\u003c");
|
|
33340
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Ignotum ID · Development</title>
|
|
33341
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><style>
|
|
33342
|
+
body{font:16px system-ui;background:#f7f7f5;color:#20211f;max-width:420px;margin:8vh auto;padding:24px}
|
|
33343
|
+
label{display:block;margin:16px 0}input:not([type=checkbox]){box-sizing:border-box;width:100%;padding:10px;border:1px solid #bbb;border-radius:6px}
|
|
33344
|
+
button{padding:12px 18px;border:0;border-radius:6px;background:#222;color:white;cursor:pointer;margin-right:8px}
|
|
33345
|
+
p{line-height:1.5}small{color:#666}</style></head><body><h1>Ignotum ID</h1><p>Choose a local username to try your app.</p>
|
|
33346
|
+
<form id="form"><label>Username<input name="username" required pattern="[a-z][a-z0-9_-]{0,63}" placeholder="john"></label>
|
|
33347
|
+
<label>Name<input name="name"></label><label>Email<input name="email" type="email"></label><label>Image URL<input name="image" type="url"></label>
|
|
33348
|
+
${[
|
|
33349
|
+
"name",
|
|
33350
|
+
"email",
|
|
33351
|
+
"image"
|
|
33352
|
+
].map((field) => `<label><input type="checkbox" name="share" value="${escapeHtml(field)}"> Share ${escapeHtml(field)}</label>`).join("")}
|
|
33353
|
+
<label><input type="checkbox" name="manage"> Replace existing sharing choices</label>
|
|
33354
|
+
<p><small>Profile sharing is optional. No password or account is needed in development.</small></p>
|
|
33355
|
+
<button>Continue</button><button type="button" id="cancel">Cancel</button><p id="error"></p></form>
|
|
33356
|
+
<script type="module">
|
|
33357
|
+
const request=${encoded};const form=document.querySelector('#form');
|
|
33358
|
+
const previous=JSON.parse(sessionStorage.getItem('${developmentSelectionKey}')||'null');
|
|
33359
|
+
if(previous?.username)form.elements.username.value=previous.username;
|
|
33360
|
+
if(request.intent==='requestProfile')form.elements.username.readOnly=true;
|
|
33361
|
+
let loading=false;const loadProfile=async()=>{loading=true;try{const response=await fetch('${localIdPath}/profile?username='+encodeURIComponent(form.elements.username.value),{headers:{'x-ignotum-request':'1'}});if(!response.ok)throw new Error('Could not load the local profile.');const profile=await response.json();for(const field of ['name','email','image']){form.elements[field].value=profile?.user[field]??'';form.querySelector('input[name=share][value='+field+']').checked=profile?.share.includes(field)??false;}}finally{loading=false;}};form.elements.username.onchange=loadProfile;if(previous?.username)await loadProfile();
|
|
33362
|
+
document.querySelector('#cancel').onclick=()=>{sessionStorage.setItem('ignotum.id.outcome','Cancelled');location.replace(request.returnTo)};
|
|
33363
|
+
form.onsubmit=async(e)=>{e.preventDefault();if(loading)return;const data=new FormData(form);
|
|
33364
|
+
try{const response=await fetch('${localIdPath}',{method:'POST',headers:{'content-type':'application/json','x-ignotum-request':'1'},body:JSON.stringify({request,username:data.get('username'),name:data.get('name'),email:data.get('email'),image:data.get('image'),share:data.getAll('share'),manage:data.get('manage')==='on'})});
|
|
33365
|
+
if(!response.ok)throw new Error('Could not update your local ID.');
|
|
33366
|
+
sessionStorage.setItem('${developmentSelectionKey}',JSON.stringify({formatVersion:1,username:data.get('username'),epoch:request.intent==='requestProfile'?previous.epoch:'dev:'+crypto.randomUUID()}));
|
|
33367
|
+
sessionStorage.setItem('ignotum.id.outcome',request.intent==='requestProfile'?'ProfileUpdated':'SignedIn');location.replace(request.returnTo);
|
|
33368
|
+
}catch(error){document.querySelector('#error').textContent=error.message}}
|
|
33369
|
+
<\/script></body></html>`;
|
|
33370
|
+
};
|
|
33371
|
+
//#endregion
|
|
33372
|
+
//#region ../../node_modules/.pnpm/@effect+sql-sqlite-node@4.0.0-rc.111_effect@4.0.0-rc.111/node_modules/@effect/sql-sqlite-node/dist/SqliteClient.js
|
|
33373
|
+
/**
|
|
33374
|
+
* Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
|
|
33375
|
+
*
|
|
33376
|
+
* This module opens a SQLite database and exposes it as both `SqliteClient` and
|
|
33377
|
+
* the generic Effect SQL client. It serializes access through one connection,
|
|
33378
|
+
* caches prepared statements, enables WAL mode unless disabled, and waits up
|
|
33379
|
+
* to five seconds for busy databases by default. Explicit transactions on
|
|
33380
|
+
* writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock
|
|
33381
|
+
* upgrades, which serializes them behind other writers even when they only
|
|
33382
|
+
* read. Clients opened with `readonly: true` are unaffected. Busy waits block
|
|
33383
|
+
* the Node.js event loop because `node:sqlite` is synchronous. Database backup
|
|
33384
|
+
* and extension loading are supported; streaming queries and `updateValues`
|
|
33385
|
+
* are not.
|
|
33386
|
+
*
|
|
33387
|
+
* @since 4.0.0
|
|
33388
|
+
*/
|
|
33389
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
33390
|
+
const MAX_BUSY_TIMEOUT = 2147483647;
|
|
33391
|
+
/**
|
|
33392
|
+
* Runtime type identifier used to mark Node `SqliteClient` values.
|
|
33393
|
+
*
|
|
33394
|
+
* @category type IDs
|
|
33395
|
+
* @since 4.0.0
|
|
33396
|
+
*/
|
|
33397
|
+
const TypeId$1 = "~@effect/sql-sqlite-node/SqliteClient";
|
|
33398
|
+
/**
|
|
33399
|
+
* Service tag for the node SQLite client implementation.
|
|
33400
|
+
*
|
|
33401
|
+
* @category services
|
|
33402
|
+
* @since 4.0.0
|
|
33403
|
+
*/
|
|
33404
|
+
const SqliteClient = /*#__PURE__*/ Context$1.Service("@effect/sql-sqlite-node/SqliteClient");
|
|
33405
|
+
/**
|
|
33406
|
+
* Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL and a 5-second busy timeout enabled by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected.
|
|
33407
|
+
*
|
|
33408
|
+
* @category constructors
|
|
33409
|
+
* @since 4.0.0
|
|
33410
|
+
*/
|
|
33411
|
+
const make$2 = (options) => Effect$1.gen(function* () {
|
|
33412
|
+
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
|
|
33413
|
+
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : void 0;
|
|
33414
|
+
const makeConnection = Effect$1.gen(function* () {
|
|
33415
|
+
const scope = yield* Effect$1.scope;
|
|
33416
|
+
const db = new DatabaseSync(options.filename, {
|
|
33417
|
+
readOnly: options.readonly ?? false,
|
|
33418
|
+
allowExtension: true
|
|
33419
|
+
});
|
|
33420
|
+
yield* Scope.addFinalizer(scope, Effect$1.sync(() => db.close()));
|
|
33421
|
+
db.enableLoadExtension(false);
|
|
33422
|
+
const busyTimeout = Math.min(MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))));
|
|
33423
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
33424
|
+
if (options.disableWAL !== true) db.exec("PRAGMA journal_mode = WAL");
|
|
33425
|
+
const prepareCache = yield* Cache.make({
|
|
33426
|
+
capacity: options.prepareCacheSize ?? 200,
|
|
33427
|
+
timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
|
|
33428
|
+
lookup: (sql) => Effect$1.try({
|
|
33429
|
+
try: () => db.prepare(sql),
|
|
33430
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
|
|
33431
|
+
})
|
|
33432
|
+
});
|
|
33433
|
+
const runStatement = (statement, params, raw) => Effect$1.withFiber((fiber) => {
|
|
33434
|
+
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
33435
|
+
return Effect$1.try({
|
|
33436
|
+
try: () => {
|
|
33437
|
+
statement.setReadBigInts(useSafeIntegers);
|
|
33438
|
+
if (statement.columns().length > 0) return statement.all(...params);
|
|
33439
|
+
const result = statement.run(...params);
|
|
33440
|
+
return raw ? {
|
|
33441
|
+
changes: result.changes,
|
|
33442
|
+
lastInsertRowid: result.lastInsertRowid
|
|
33443
|
+
} : [];
|
|
33444
|
+
},
|
|
33445
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
33446
|
+
});
|
|
33447
|
+
});
|
|
33448
|
+
const runStatementValues = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
33449
|
+
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
33450
|
+
return Effect$1.try({
|
|
33451
|
+
try: () => {
|
|
33452
|
+
statement.setReadBigInts(useSafeIntegers);
|
|
33453
|
+
if (statement.columns().length > 0) return statement.all(...params);
|
|
33454
|
+
statement.run(...params);
|
|
33455
|
+
return [];
|
|
33456
|
+
},
|
|
33457
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
33458
|
+
});
|
|
33459
|
+
});
|
|
33460
|
+
const runStatementValuesUnprepared = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
33461
|
+
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
33462
|
+
return Effect$1.try({
|
|
33463
|
+
try: () => {
|
|
33464
|
+
statement.setReadBigInts(useSafeIntegers);
|
|
33465
|
+
statement.setReturnArrays(true);
|
|
33466
|
+
if (statement.columns().length > 0) return statement.all(...params);
|
|
33467
|
+
statement.run(...params);
|
|
33468
|
+
return [];
|
|
33469
|
+
},
|
|
33470
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
33471
|
+
});
|
|
33472
|
+
});
|
|
33473
|
+
const run = (sql, params, raw = false) => Effect$1.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
|
|
33474
|
+
const runValues = (sql, params) => Effect$1.acquireUseRelease(Cache.get(prepareCache, sql), (statement) => {
|
|
33475
|
+
statement.setReturnArrays(true);
|
|
33476
|
+
return runStatementValues(statement, params);
|
|
33477
|
+
}, (statement) => Effect$1.sync(() => statement.setReturnArrays(false)));
|
|
33478
|
+
const runValuesUnprepared = (sql, params) => runStatementValuesUnprepared(db.prepare(sql), params);
|
|
33479
|
+
return identity({
|
|
33480
|
+
execute(sql, params, transformRows) {
|
|
33481
|
+
return transformRows ? Effect$1.map(run(sql, params), transformRows) : run(sql, params);
|
|
33482
|
+
},
|
|
33483
|
+
executeRaw(sql, params) {
|
|
33484
|
+
return run(sql, params, true);
|
|
33485
|
+
},
|
|
33486
|
+
executeValues(sql, params) {
|
|
33487
|
+
return runValues(sql, params);
|
|
33488
|
+
},
|
|
33489
|
+
executeValuesUnprepared(sql, params) {
|
|
33490
|
+
return runValuesUnprepared(sql, params);
|
|
33491
|
+
},
|
|
33492
|
+
executeUnprepared(sql, params, transformRows) {
|
|
33493
|
+
const effect = runStatement(db.prepare(sql), params ?? [], false);
|
|
33494
|
+
return transformRows ? Effect$1.map(effect, transformRows) : effect;
|
|
33495
|
+
},
|
|
33496
|
+
executeStream(_sql, _params) {
|
|
33497
|
+
return Stream$1.die("executeStream not implemented");
|
|
33498
|
+
},
|
|
33499
|
+
backup(destination) {
|
|
33500
|
+
return Effect$1.suspend(() => {
|
|
33501
|
+
let totalPages = 0;
|
|
33502
|
+
return Effect$1.tryPromise({
|
|
33503
|
+
try: () => backup(db, destination, { progress: (progress) => {
|
|
33504
|
+
totalPages = progress.totalPages;
|
|
33505
|
+
} }).then((pages) => ({
|
|
33506
|
+
totalPages: totalPages || pages,
|
|
33507
|
+
remainingPages: 0
|
|
33508
|
+
})),
|
|
33509
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
|
|
33510
|
+
});
|
|
33511
|
+
});
|
|
33512
|
+
},
|
|
33513
|
+
loadExtension(path) {
|
|
33514
|
+
return Effect$1.acquireUseRelease(Effect$1.sync(() => db.enableLoadExtension(true)), () => Effect$1.try({
|
|
33515
|
+
try: () => db.loadExtension(path),
|
|
33516
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
|
|
33517
|
+
}), () => Effect$1.sync(() => db.enableLoadExtension(false)));
|
|
33518
|
+
}
|
|
33519
|
+
});
|
|
33520
|
+
});
|
|
33521
|
+
const semaphore = yield* Semaphore$1.make(1);
|
|
33522
|
+
const connection = yield* makeConnection;
|
|
33523
|
+
const acquirer = semaphore.withPermits(1)(Effect$1.succeed(connection));
|
|
33524
|
+
const transactionAcquirer = Effect$1.uninterruptibleMask((restore) => {
|
|
33525
|
+
const fiber = Fiber.getCurrent();
|
|
33526
|
+
const scope = Context$1.getUnsafe(fiber.context, Scope.Scope);
|
|
33527
|
+
return Effect$1.as(Effect$1.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection);
|
|
33528
|
+
});
|
|
33529
|
+
return Object.assign(yield* Client.make({
|
|
33530
|
+
acquirer,
|
|
33531
|
+
compiler,
|
|
33532
|
+
transactionAcquirer,
|
|
33533
|
+
beginTransaction: "BEGIN IMMEDIATE",
|
|
33534
|
+
spanAttributes: [...options.spanAttributes ? Object.entries(options.spanAttributes) : [], [ATTR_DB_SYSTEM_NAME, "sqlite"]],
|
|
33535
|
+
transformRows
|
|
33536
|
+
}), {
|
|
33537
|
+
[TypeId$1]: TypeId$1,
|
|
33538
|
+
config: options,
|
|
33539
|
+
backup: (destination) => Effect$1.flatMap(acquirer, (_) => _.backup(destination)),
|
|
33540
|
+
loadExtension: (path) => Effect$1.flatMap(acquirer, (_) => _.loadExtension(path))
|
|
33541
|
+
});
|
|
33542
|
+
});
|
|
33543
|
+
/**
|
|
33544
|
+
* Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
|
|
33545
|
+
*
|
|
33546
|
+
* @category layers
|
|
33547
|
+
* @since 4.0.0
|
|
33548
|
+
*/
|
|
33549
|
+
const layer = (config) => Layer$1.effectContext(Effect$1.map(make$2(config), (client) => Context$1.make(SqliteClient, client).pipe(Context$1.add(Client.SqlClient, client)))).pipe(Layer$1.provide(Reactivity.layer));
|
|
33550
|
+
const classifyError = (cause, message, operation) => classifySqliteError(sqliteCauseWithErrno(cause), {
|
|
33551
|
+
message,
|
|
33552
|
+
operation
|
|
33553
|
+
});
|
|
33554
|
+
const sqliteCauseWithErrno = (cause) => {
|
|
33555
|
+
if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) return cause;
|
|
33556
|
+
const errcode = cause.errcode;
|
|
33557
|
+
if (typeof errcode !== "number") return cause;
|
|
33558
|
+
return Object.assign(cause, { errno: errcode });
|
|
33559
|
+
};
|
|
33560
|
+
//#endregion
|
|
33561
|
+
//#region ../runtime/dist/functions.js
|
|
33562
|
+
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
33563
|
+
//#endregion
|
|
33564
|
+
//#region ../contracts/dist/runtime/transport.js
|
|
33565
|
+
const runtimeInvocationPath = "/v1/invoke";
|
|
33566
|
+
const runtimeRevisionPath = "/v1/revision";
|
|
33567
|
+
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
33568
|
+
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
33569
|
+
const runtimeFileEffectsPath = "/v1/files/effects";
|
|
33570
|
+
const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
|
|
33571
|
+
const ignotumPathPrefix$1 = "/_ignotum";
|
|
33572
|
+
const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
|
|
33573
|
+
const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
|
|
33574
|
+
const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
|
|
33575
|
+
const RuntimeRequestPath = Schema.Literals([
|
|
33576
|
+
runtimeInvocationPath,
|
|
33577
|
+
runtimeRevisionPath,
|
|
33578
|
+
runtimeIndexPreparePath,
|
|
33579
|
+
runtimeIndexCommitPath,
|
|
33580
|
+
runtimeFileEffectsPath,
|
|
33581
|
+
runtimeFileEffectsAcknowledgePath
|
|
33582
|
+
]);
|
|
33583
|
+
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
33584
|
+
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
33585
|
+
Schema.Struct({
|
|
33586
|
+
appId: AppId,
|
|
33587
|
+
deploymentId: Schema.optional(DeploymentId),
|
|
33588
|
+
nonce: RuntimeRequestNonce,
|
|
33589
|
+
timestamp: RuntimeRequestTimestamp,
|
|
33590
|
+
signature: RuntimeRequestSignature
|
|
33591
|
+
});
|
|
33592
|
+
Schema.Struct({
|
|
33593
|
+
method: Schema.Literal("POST"),
|
|
33594
|
+
path: RuntimeRequestPath,
|
|
33595
|
+
timestamp: RuntimeRequestTimestamp,
|
|
33596
|
+
nonce: RuntimeRequestNonce,
|
|
33597
|
+
appId: AppId,
|
|
33598
|
+
deploymentId: Schema.optional(DeploymentId),
|
|
33599
|
+
bodySha256: Sha256
|
|
33600
|
+
});
|
|
33601
|
+
Schema.Struct({
|
|
33602
|
+
appId: AppId,
|
|
33603
|
+
deploymentId: DeploymentId
|
|
33604
|
+
});
|
|
33605
|
+
Schema.Struct({ completed: Schema.Literal(true) });
|
|
33606
|
+
const RuntimeErrorCode = Schema.Literals([
|
|
33607
|
+
"InvalidRequest",
|
|
33608
|
+
"InvalidSignature",
|
|
33609
|
+
"RequestExpired",
|
|
33610
|
+
"RequestReplay",
|
|
33611
|
+
"InvocationRejected",
|
|
33612
|
+
"RuntimeUnavailable"
|
|
33613
|
+
]);
|
|
33614
|
+
Schema.Struct({
|
|
33615
|
+
code: RuntimeErrorCode,
|
|
33616
|
+
message: Schema.String,
|
|
33617
|
+
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
33618
|
+
});
|
|
33619
|
+
//#endregion
|
|
33620
|
+
//#region ../shared/dist/http-body.js
|
|
33621
|
+
const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => stream.pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.mkUint8Array));
|
|
33297
33622
|
const syncPath = appSyncPath;
|
|
33298
33623
|
const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
|
|
33299
33624
|
//#endregion
|
|
@@ -34103,9 +34428,15 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
34103
34428
|
dates
|
|
34104
34429
|
};
|
|
34105
34430
|
}));
|
|
34106
|
-
return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved) {
|
|
34431
|
+
return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved, identity) {
|
|
34432
|
+
let readId = false;
|
|
34433
|
+
const id = makeIdContext(Effect.sync(() => {
|
|
34434
|
+
readId = true;
|
|
34435
|
+
return identity?.user ?? null;
|
|
34436
|
+
}));
|
|
34107
34437
|
const invoke = (context) => Effect.gen(() => resolved.definition.handler(Object.freeze({
|
|
34108
34438
|
...context,
|
|
34439
|
+
id,
|
|
34109
34440
|
env: resolved.environment ?? Object.freeze({})
|
|
34110
34441
|
}), resolved.args)).pipe(Effect.matchEffect({
|
|
34111
34442
|
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
@@ -34116,7 +34447,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
34116
34447
|
const result = {
|
|
34117
34448
|
type: "Query",
|
|
34118
34449
|
result: value,
|
|
34119
|
-
dependencies,
|
|
34450
|
+
dependencies: readId ? [...dependencies, { type: "Id" }] : dependencies,
|
|
34120
34451
|
observedRevision
|
|
34121
34452
|
};
|
|
34122
34453
|
return files.length === 0 ? result : {
|
|
@@ -34155,7 +34486,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
34155
34486
|
return {
|
|
34156
34487
|
type: "Query",
|
|
34157
34488
|
result,
|
|
34158
|
-
dependencies: [],
|
|
34489
|
+
dependencies: readId ? [{ type: "Id" }] : [],
|
|
34159
34490
|
observedRevision: yield* database.revision
|
|
34160
34491
|
};
|
|
34161
34492
|
})));
|
|
@@ -34166,9 +34497,9 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
34166
34497
|
const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function* () {
|
|
34167
34498
|
const registry = yield* FunctionRegistry;
|
|
34168
34499
|
const executor = yield* FunctionExecutor;
|
|
34169
|
-
return FunctionRuntime.of({ prepare: Effect.fn("DevFunctionRuntime.prepare")(function* (functionAddress, kind, args) {
|
|
34500
|
+
return FunctionRuntime.of({ prepare: Effect.fn("DevFunctionRuntime.prepare")(function* (functionAddress, kind, args, identity) {
|
|
34170
34501
|
const resolved = yield* registry.resolve(functionAddress, kind, args);
|
|
34171
|
-
return { execute: executor.execute(functionAddress, kind, resolved) };
|
|
34502
|
+
return { execute: executor.execute(functionAddress, kind, resolved, identity) };
|
|
34172
34503
|
}) });
|
|
34173
34504
|
}));
|
|
34174
34505
|
//#endregion
|
|
@@ -34429,17 +34760,19 @@ const localSyncIdentity = {
|
|
|
34429
34760
|
};
|
|
34430
34761
|
const reloadEnvironmentAndInvalidate = (reload, publish) => reload.pipe(Effect.ensuring(publish));
|
|
34431
34762
|
const queryInvalidationLayer = Layer.effect(QueryInvalidation, Effect.gen(function* () {
|
|
34763
|
+
const cache = yield* LocalQueryCache;
|
|
34432
34764
|
const pubsub = yield* PubSub.unbounded();
|
|
34433
34765
|
const latestRevision = yield* Ref.make(AppStateRevision.make(0));
|
|
34434
34766
|
return QueryInvalidation.of({
|
|
34435
34767
|
latestRevision: Ref.get(latestRevision),
|
|
34436
34768
|
publish: (event) => Effect.gen(function* () {
|
|
34769
|
+
cache.invalidate(event);
|
|
34437
34770
|
if (event.type === "Dependencies") yield* Ref.update(latestRevision, (current) => AppStateRevision.make(Math.max(current, event.committedRevision)));
|
|
34438
34771
|
yield* PubSub.publish(pubsub, event);
|
|
34439
34772
|
}).pipe(Effect.asVoid),
|
|
34440
34773
|
subscribe: PubSub.subscribe(pubsub)
|
|
34441
34774
|
});
|
|
34442
|
-
}));
|
|
34775
|
+
})).pipe(Layer.provideMerge(LocalQueryCache.layer));
|
|
34443
34776
|
var InvocationIdConflict = class extends Schema.TaggedError()("InvocationIdConflict", { id: Schema.String }) {};
|
|
34444
34777
|
const MutationReplayRow = Schema.Struct({
|
|
34445
34778
|
input: Schema.String,
|
|
@@ -34495,10 +34828,17 @@ const syncError = (code, message, operation) => {
|
|
|
34495
34828
|
operation
|
|
34496
34829
|
};
|
|
34497
34830
|
};
|
|
34498
|
-
const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
34831
|
+
const runSession = Effect.fn("SyncServer.runSession")(function* (socket, initialIdentity, refreshIdentity) {
|
|
34832
|
+
let identity = initialIdentity ?? {
|
|
34833
|
+
sessionEpoch: "dev:anonymous",
|
|
34834
|
+
user: null,
|
|
34835
|
+
viewRevision: 0,
|
|
34836
|
+
validUntil: Number.MAX_SAFE_INTEGER
|
|
34837
|
+
};
|
|
34499
34838
|
const runtime = yield* FunctionRuntime;
|
|
34500
34839
|
const invalidation = yield* QueryInvalidation;
|
|
34501
34840
|
const mutationReplay = yield* MutationReplay;
|
|
34841
|
+
const queryCache = yield* LocalQueryCache;
|
|
34502
34842
|
const files = yield* LocalApplicationFiles;
|
|
34503
34843
|
const subscriptions = yield* Ref.make(HashMap.empty());
|
|
34504
34844
|
const dependencyIndex = makeDependencyIndex();
|
|
@@ -34507,7 +34847,11 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34507
34847
|
const writeSemaphore = yield* Semaphore.make(1);
|
|
34508
34848
|
const write = yield* socket.writer;
|
|
34509
34849
|
const send = Effect.fn("SyncServer.send")(function* (message) {
|
|
34510
|
-
|
|
34850
|
+
const bound = message.type === "Result" || message.type === "Preparation" || message.type === "Error" ? {
|
|
34851
|
+
...message,
|
|
34852
|
+
sessionEpoch: identity.sessionEpoch
|
|
34853
|
+
} : message;
|
|
34854
|
+
yield* writeSemaphore.withPermits(1)(Schema.encodeEffect(ServerMessageJson)(bound).pipe(Effect.flatMap(write)));
|
|
34511
34855
|
});
|
|
34512
34856
|
const sendResolutionError = (operation, error, deliver = send) => {
|
|
34513
34857
|
const code = error._tag;
|
|
@@ -34523,7 +34867,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34523
34867
|
let prepared = previouslyPrepared;
|
|
34524
34868
|
while (yield* isActive(subscriptionId, subscription)) {
|
|
34525
34869
|
if (prepared === void 0) {
|
|
34526
|
-
const resolved = yield* runtime.prepare(subscription.function, "Query", subscription.args).pipe(Effect.catchTags({
|
|
34870
|
+
const resolved = yield* runtime.prepare(subscription.function, "Query", subscription.args, identity).pipe(Effect.catchTags({
|
|
34527
34871
|
FunctionUnavailable: (error) => sendResolutionError(operation, error, deliver),
|
|
34528
34872
|
InvalidArguments: (error) => sendResolutionError(operation, error, deliver),
|
|
34529
34873
|
UnknownFunction: (error) => sendResolutionError(operation, error, deliver),
|
|
@@ -34532,7 +34876,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34532
34876
|
if (resolved === void 0) return;
|
|
34533
34877
|
prepared = resolved;
|
|
34534
34878
|
}
|
|
34535
|
-
const result = yield* prepared.execute;
|
|
34879
|
+
const result = yield* queryCache.execute(subscription.queryKey, identity, yield* invalidation.latestRevision, prepared.execute);
|
|
34536
34880
|
prepared = void 0;
|
|
34537
34881
|
if (result.type !== "Query") return;
|
|
34538
34882
|
if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
|
|
@@ -34543,7 +34887,12 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34543
34887
|
type: "Snapshot",
|
|
34544
34888
|
id: subscriptionId,
|
|
34545
34889
|
result: result.result,
|
|
34546
|
-
revision: result.observedRevision
|
|
34890
|
+
revision: result.observedRevision,
|
|
34891
|
+
identity: {
|
|
34892
|
+
sessionEpoch: identity.sessionEpoch,
|
|
34893
|
+
viewRevision: identity.viewRevision,
|
|
34894
|
+
dependsOnId: result.dependencies.some((dependency) => dependency.type === "Id")
|
|
34895
|
+
}
|
|
34547
34896
|
};
|
|
34548
34897
|
yield* deliver(granted.length === 0 ? snapshot : {
|
|
34549
34898
|
...snapshot,
|
|
@@ -34561,6 +34910,17 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34561
34910
|
}))), FiberSet.run(invocationFibers), Effect.asVoid);
|
|
34562
34911
|
};
|
|
34563
34912
|
const refreshAll = Effect.fn("SyncServer.refreshAll")(function* () {
|
|
34913
|
+
if (refreshIdentity !== void 0) {
|
|
34914
|
+
const next = yield* refreshIdentity;
|
|
34915
|
+
const changed = idViewKey(identity) !== idViewKey(next) || identity.validUntil !== next.validUntil;
|
|
34916
|
+
identity = next;
|
|
34917
|
+
if (changed) yield* send({
|
|
34918
|
+
type: "Session",
|
|
34919
|
+
event: "Updated",
|
|
34920
|
+
session: identity,
|
|
34921
|
+
serverTime: yield* Clock.currentTimeMillis
|
|
34922
|
+
});
|
|
34923
|
+
}
|
|
34564
34924
|
const current = yield* Ref.get(subscriptions);
|
|
34565
34925
|
yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) => scheduleQuery(subscriptionId, subscription));
|
|
34566
34926
|
});
|
|
@@ -34584,7 +34944,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34584
34944
|
yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
|
|
34585
34945
|
return;
|
|
34586
34946
|
}
|
|
34587
|
-
const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
|
|
34947
|
+
const prepared = yield* runtime.prepare(message.function, "Query", message.args, identity).pipe(Effect.catchTags({
|
|
34588
34948
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
34589
34949
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
34590
34950
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
@@ -34609,7 +34969,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34609
34969
|
yield* files.releasePreparation(message.id);
|
|
34610
34970
|
return;
|
|
34611
34971
|
}
|
|
34612
|
-
const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
|
|
34972
|
+
const prepared = yield* runtime.prepare(message.function, "Mutation", message.args, identity).pipe(Effect.catchTags({
|
|
34613
34973
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
34614
34974
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
34615
34975
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
@@ -34619,7 +34979,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34619
34979
|
yield* files.releasePreparation(message.id);
|
|
34620
34980
|
return;
|
|
34621
34981
|
}
|
|
34622
|
-
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
|
|
34982
|
+
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args, identity.sessionEpoch);
|
|
34623
34983
|
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) => {
|
|
34624
34984
|
if (!("committedRevision" in result)) return send({
|
|
34625
34985
|
type: "Result",
|
|
@@ -34676,7 +35036,9 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34676
35036
|
});
|
|
34677
35037
|
yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
|
|
34678
35038
|
type: "Handshake",
|
|
34679
|
-
...localSyncIdentity
|
|
35039
|
+
...localSyncIdentity,
|
|
35040
|
+
session: identity,
|
|
35041
|
+
serverTime: yield* Clock.currentTimeMillis
|
|
34680
35042
|
}).pipe(Effect.orDie) }).pipe(Effect.ensuring(Ref.get(subscriptions).pipe(Effect.flatMap((active) => Effect.forEach(HashMap.keys(active), (subscriptionId) => files.releaseGrants(subscriptionId))), Effect.asVoid)));
|
|
34681
35043
|
});
|
|
34682
35044
|
var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
|
|
@@ -34686,8 +35048,9 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
34686
35048
|
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(IdGenerator.layer));
|
|
34687
35049
|
const environmentLayer = DevelopmentEnvironment.layer(server, appDirectory).pipe(Layer.provide(layer$1), Layer.orDie);
|
|
34688
35050
|
const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(Layer.merge(layer$1, environmentLayer))), executorLayer)));
|
|
34689
|
-
const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, environmentLayer, queryInvalidationLayer, persistenceLayer, layer$1, localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(IdGenerator.layer, sqliteLayer, layer$1))));
|
|
35051
|
+
const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, environmentLayer, queryInvalidationLayer, persistenceLayer, layer$1, LocalId.layer(appDirectory).pipe(Layer.provide(layer$1)), localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(IdGenerator.layer, sqliteLayer, layer$1))));
|
|
34690
35052
|
return Layer.effect(SyncHandlers, Effect.gen(function* () {
|
|
35053
|
+
const localId = yield* LocalId;
|
|
34691
35054
|
const invalidation = yield* QueryInvalidation;
|
|
34692
35055
|
const environment = yield* DevelopmentEnvironment;
|
|
34693
35056
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -34702,7 +35065,49 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
34702
35065
|
}));
|
|
34703
35066
|
const httpApp = Effect.gen(function* () {
|
|
34704
35067
|
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
34705
|
-
const
|
|
35068
|
+
const url = new URL(request.url, `http://${request.headers.host ?? "ignotum.local"}`);
|
|
35069
|
+
const pathname = url.pathname;
|
|
35070
|
+
if (pathname === "/_ignotum/v1/session" && request.method === "GET") {
|
|
35071
|
+
const session = yield* localId.session(url.searchParams.get("username"), url.searchParams.get("epoch") ?? "dev:anonymous");
|
|
35072
|
+
return yield* HttpServerResponse.json({
|
|
35073
|
+
...session,
|
|
35074
|
+
serverTime: yield* Clock.currentTimeMillis,
|
|
35075
|
+
expiresAt: session.validUntil,
|
|
35076
|
+
development: true
|
|
35077
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
35078
|
+
}
|
|
35079
|
+
if (pathname === sessionRequestsPath && request.method === "POST") {
|
|
35080
|
+
if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
|
|
35081
|
+
const input = yield* request.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SessionRequest)), Effect.option);
|
|
35082
|
+
if (Option.isNone(input)) return HttpServerResponse.empty({ status: 400 });
|
|
35083
|
+
return yield* HttpServerResponse.json({
|
|
35084
|
+
redirectUrl: `${localIdPath}?request=${encodeURIComponent(JSON.stringify(input.value))}`,
|
|
35085
|
+
expiresAt: (yield* Clock.currentTimeMillis) + 6e5
|
|
35086
|
+
}, {
|
|
35087
|
+
status: 201,
|
|
35088
|
+
headers: { "cache-control": "no-store" }
|
|
35089
|
+
});
|
|
35090
|
+
}
|
|
35091
|
+
if (pathname === `/_ignotum/v1/id/profile` && request.method === "GET") {
|
|
35092
|
+
if (request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
|
|
35093
|
+
return yield* HttpServerResponse.json((yield* localId.profile(url.searchParams.get("username") ?? "")) ?? null, { headers: { "cache-control": "no-store" } });
|
|
35094
|
+
}
|
|
35095
|
+
if (pathname === "/_ignotum/v1/id" && request.method === "GET") {
|
|
35096
|
+
const input = yield* Schema.decodeEffect(Schema.fromJsonString(SessionRequest))(url.searchParams.get("request") ?? "").pipe(Effect.option);
|
|
35097
|
+
return Option.isNone(input) ? HttpServerResponse.empty({ status: 400 }) : HttpServerResponse.html(localIdPage(input.value));
|
|
35098
|
+
}
|
|
35099
|
+
if (pathname === "/_ignotum/v1/id" && request.method === "POST") {
|
|
35100
|
+
if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
|
|
35101
|
+
const input = yield* request.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(LocalCompletion)), Effect.option);
|
|
35102
|
+
if (Option.isNone(input)) return HttpServerResponse.empty({ status: 400 });
|
|
35103
|
+
yield* localId.complete(input.value);
|
|
35104
|
+
yield* invalidation.publish({ type: "All" });
|
|
35105
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
35106
|
+
}
|
|
35107
|
+
if (pathname === "/_ignotum/v1/session" && request.method === "DELETE") {
|
|
35108
|
+
if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
|
|
35109
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
35110
|
+
}
|
|
34706
35111
|
if (pathname.startsWith(fileUploadUrlPrefix)) {
|
|
34707
35112
|
if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
|
|
34708
35113
|
const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
|
|
@@ -34736,8 +35141,13 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
34736
35141
|
}, { status: pathname === syncPath ? 426 : 404 });
|
|
34737
35142
|
});
|
|
34738
35143
|
const socketApp = Effect.gen(function* () {
|
|
34739
|
-
const
|
|
34740
|
-
|
|
35144
|
+
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
35145
|
+
const url = new URL(request.url, `http://${request.headers.host ?? "ignotum.local"}`);
|
|
35146
|
+
if (request.headers.origin !== url.origin) return HttpServerResponse.empty({ status: 403 });
|
|
35147
|
+
const refresh = localId.session(url.searchParams.get("username"), url.searchParams.get("epoch") ?? "dev:anonymous");
|
|
35148
|
+
const identity = yield* refresh;
|
|
35149
|
+
const socket = yield* request.upgrade;
|
|
35150
|
+
yield* runSession(socket, identity, refresh);
|
|
34741
35151
|
return HttpServerResponse.empty();
|
|
34742
35152
|
});
|
|
34743
35153
|
const http = yield* makeHandler(httpApp, { scope });
|
|
@@ -34914,6 +35324,15 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
|
|
|
34914
35324
|
}));
|
|
34915
35325
|
});
|
|
34916
35326
|
//#endregion
|
|
35327
|
+
//#region ../../docs/user/authentication.md?raw
|
|
35328
|
+
var authentication_default = "---\ntitle: Ignotum ID\ndescription: Sign users in and read their identity in queries and mutations.\nsection: Authentication\nslug: authentication\norder: 40\n---\n\n# Ignotum ID\n\nIgnotum ID lets people sign in to your app with Google. It is available in every app without an\nauthentication configuration file. The account people use in your app is separate from the\nIgnotum account you use to deploy it.\n\n## Add sign-in to your app\n\nUse `useId` from `ignotum/client`:\n\n```tsx\nimport { useId } from \"ignotum/client\";\n\nexport function Account() {\n const id = useId();\n\n if (id.status === \"pending\") return <p>Loading account...</p>;\n if (id.status === \"error\") return <p>{id.error}</p>;\n if (id.status === \"signedOut\") {\n return <button onClick={() => id.signIn()}>Sign in</button>;\n }\n\n return (\n <div>\n <p>{id.user.name ?? \"Signed in\"}</p>\n <button onClick={() => id.signOut()}>Sign out</button>\n </div>\n );\n}\n```\n\n`signIn()` opens Ignotum ID and returns to the current page when the person finishes or cancels.\nPass `returnTo: \"/account\"` to choose another page in your app. The destination must be a relative\npath within the app. The action's promise reports whether the redirect started. After returning,\n`id.outcome` reports `SignedIn`, `ProfileUpdated`, or `Cancelled` when available.\n\nSign-out ends this app's session. People can manage all their connected apps and sign out of their\nother sessions from the Ignotum ID account page.\n\n## Read the current user\n\nQueries and mutations receive `ctx.id`. Use `current()` when signed-out visitors are allowed:\n\n```ts\nconst user = yield * ctx.id.current();\nif (user === null) return [];\n```\n\nUse `require()` when the function needs a signed-in user:\n\n```ts\nconst user = yield * ctx.id.require();\n```\n\n`require()` returns the application error `IdRequired` when the visitor is signed out. Handle it\nwith `Result.match` as you would another application error.\n\nThe returned user has an `id` and optional `name`, `email`, and `image` fields. The same person has\na stable ID within your app, including after disconnecting and signing in again. Their ID differs\nbetween apps. Use the ID for ownership checks. Names and email addresses can change and should\nnot be used as ownership keys.\n\nIgnotum updates queries that read `ctx.id` when the user's shared profile changes. After sign-out\nor a loss of session verification, results that depend on identity return to pending until they\ncan be loaded for the current visitor.\n\n## Store ownership\n\nUse `values.userId()` for an Ignotum ID user reference:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n todos: table({\n ownerId: values.userId(),\n text: values.string(),\n }).index(\"by_owner\", [\"ownerId\"]),\n}));\n```\n\nSet the owner from `ctx.id.require()` in your mutation. Do not trust an owner ID supplied by Web\ncode. Check ownership before returning, updating, or deleting a private document. Signing in\nidentifies the caller; your functions decide which documents that caller may access.\n\nThe `User` and `UserId` types are exported from `ignotum/server` and `ignotum/client`.\n\nNext: [Profile sharing](profile-sharing.md)\n";
|
|
35329
|
+
//#endregion
|
|
35330
|
+
//#region ../../docs/user/profile-sharing.md?raw
|
|
35331
|
+
var profile_sharing_default = "---\ntitle: Profile sharing\ndescription: Request optional profile information and handle changes to sharing.\nsection: Authentication\nslug: profile-sharing\norder: 41\n---\n\n# Profile sharing\n\nSigning in gives your app a user ID. Profile information is optional. Request the fields your app\nneeds when starting sign-in:\n\n```tsx\nconst id = useId();\nawait id.signIn({ profile: [\"name\", \"image\"] });\n```\n\nPeople choose which requested fields to share. A person can finish sign-in without sharing any of\nthem. Always handle absent fields in `id.user` and in the user returned by `ctx.id`.\n\nA signed-in person can share more information later:\n\n```tsx\nawait id.requestProfile([\"email\"], { returnTo: \"/settings\" });\n```\n\nThis request keeps the current app account. Cancelling it leaves existing sharing in place.\nRequesting profile information while signed out fails; start with `signIn()` instead.\n\nPeople can change their name and image, withdraw sharing, or disconnect your app from the Ignotum\nID account page. Your active queries and `useId` update when those changes reach the app. Keep\nprofile fields optional in the UI and use `user.id` for stored references.\n\nA shared email comes from the person's verified Google account. A shared image is a URL you can\nuse in an image element. Treat that URL as temporary and read it from the current profile instead\nof storing it as a permanent address.\n\nNext: [Test authentication locally](local-authentication.md)\n";
|
|
35332
|
+
//#endregion
|
|
35333
|
+
//#region ../../docs/user/local-authentication.md?raw
|
|
35334
|
+
var local_authentication_default = "---\ntitle: Test locally\ndescription: Try signed-in and signed-out behavior with local usernames.\nsection: Authentication\nslug: local-authentication\norder: 42\n---\n\n# Test authentication locally\n\nRun `npx ignotum dev` and use your app's sign-in button. The local sign-in page lets you choose a\nusername such as `john`, enter a profile, and select which requested fields to share. You do not\nneed a Google account or provider credentials.\n\nLocal usernames are readable user IDs. Use lowercase letters, digits, underscores, or hyphens,\nstarting with a letter. Each browser tab keeps its own selected user, so you can test two people\nusing the app at once. Signing out affects that tab.\n\nUse `ctx.id.current()`, `ctx.id.require()`, and `useId()` in the same way locally and after\ndeployment. Test signed-out visitors, two different users, and a user who declines optional\nprofile sharing. Production user IDs are opaque; do not make application behavior depend on the\nshape of a local username.\n";
|
|
35335
|
+
//#endregion
|
|
34917
35336
|
//#region ../../docs/agent/AGENTS.md?raw
|
|
34918
35337
|
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";
|
|
34919
35338
|
//#endregion
|
|
@@ -34922,6 +35341,9 @@ var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotu
|
|
|
34922
35341
|
//#endregion
|
|
34923
35342
|
//#region src/cli/agent-files.ts
|
|
34924
35343
|
const references = [
|
|
35344
|
+
["authentication.md", authentication_default],
|
|
35345
|
+
["profile-sharing.md", profile_sharing_default],
|
|
35346
|
+
["local-authentication.md", local_authentication_default],
|
|
34925
35347
|
["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_SESSION_TOKEN>\n```\n\n`ignotum auth login` obtains this revocable session through the device authorization flow. Treat it\nas an account credential. Do not include it in client code or send it from a deployed app. The API\ndoes not enable browser CORS, so call it from a CLI, CI job, or other trusted server 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"],
|
|
34926
35348
|
["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"],
|
|
34927
35349
|
["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 auth login` | Sign in with GitHub through the device authorization flow. |\n| `ignotum auth logout` | Revoke the session and remove it from this computer. |\n| `ignotum auth status` | Show the signed-in account and team. |\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## `auth`\n\nRun `npx ignotum auth login` once before deploying. The terminal prints a short code and opens the\napproval page. Confirm that the browser and terminal show the same code before approving it.\n`auth status` checks the saved session, and `auth logout` revokes it.\n\n## Environment variables\n\n| Variable | Meaning |\n| ------------------ | -------------------------------------------------------- |\n| `IGNOTUM_AUTH_URL` | Auth base URL. Defaults to `https://auth.ignotum.cloud`. |\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"],
|
|
@@ -34934,23 +35356,23 @@ const references = [
|
|
|
34934
35356
|
["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"],
|
|
34935
35357
|
["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`, `env.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, process environment, or process access. Use\nthe APIs provided through the function context. Typed server configuration is available through\n`ctx.env`; see [Environment variables](environment-variables.md).\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"],
|
|
34936
35358
|
["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. When `server/env.ts` exists, `_generated/server.ts` also types `ctx.env`; it does\nnot export environment values or `Secret`. See [Functions](functions.md) for function module naming\nrules.\n"],
|
|
34937
|
-
["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"],
|
|
35359
|
+
["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 app session, 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\nRecovering an idle hosted connection does not resend cached query results already sent to its\nsubscriptions. A new subscription still receives its initial result. Query refreshes may emit the\nsame value again, so subscription callbacks should not be treated as notifications of data changes.\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"],
|
|
34938
35360
|
["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"],
|
|
34939
35361
|
["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"],
|
|
34940
35362
|
["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"],
|
|
34941
|
-
["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\nOne server environment may contain at most 128 variables and 64 KiB of values as JSON.\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"],
|
|
34942
|
-
["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.\nChanges to `.env.ignotum` validate and reload `ctx.env`, then refresh active queries. Functions stay\nunavailable after an invalid edit until the file is fixed.\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"],
|
|
34943
|
-
["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"],
|
|
35363
|
+
["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\nOne server environment may contain at most 128 variables and 64 KiB of values as JSON.\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. When the app has no free slot, a new mutation call\nrejects with `ResourceLimitExceeded` so its promise does not stay pending.\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"],
|
|
35364
|
+
["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.\nChanges to `.env.ignotum` validate and reload `ctx.env`, then refresh active queries. Functions stay\nunavailable after an invalid edit until the file is fixed.\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\nSee [Test authentication locally](local-authentication.md) to try different users without a Google\naccount.\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"],
|
|
35365
|
+
["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. Duplicate or delayed\nresponses for a completed call do not interrupt live queries or other mutation calls.\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"],
|
|
34944
35366
|
["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"],
|
|
34945
|
-
["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
|
|
35367
|
+
["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 | `useElementId`, `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\n`useElementId` is Preact's hook for generating element IDs. `useId` reads the current\n[Ignotum ID account](authentication.md).\n"],
|
|
34946
35368
|
["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 env.ts\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. The optional `server/env.ts` declares typed,\nserver-only [environment variables](environment-variables.md). 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"],
|
|
34947
35369
|
["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"],
|
|
34948
|
-
["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
|
|
35370
|
+
["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 keeps settled public queries visible. Queries that read `ctx.id` return to\npending if their session can no longer be verified. 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"],
|
|
34949
35371
|
["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\nSign in with GitHub, then choose a globally available app slug:\n\n```sh\nnpx ignotum auth login\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 login storage, slug rules, and deployment failure behavior.\n\nNext: [How Ignotum works](how-ignotum-works.md)\n"],
|
|
34950
35372
|
["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"],
|
|
34951
35373
|
["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"],
|
|
34952
35374
|
["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"],
|
|
34953
|
-
["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\nEnvironment declarations also provide `values.secret()` and `values.secret(value)`. These produce\nredacted values that require `Secret.value(...)` for explicit access. Secret validators are not\navailable in schemas or function input and output contracts. See\n[Environment variables](environment-variables.md).\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\nMutation arguments accept objects with ordinary fields and arrays. Convert built-in collections\nsuch as `Map` and `Set` to objects or arrays, and convert `URL` instances to strings before calling\na mutation. Ignotum rejects these built-in objects instead of discarding their contents.\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"],
|
|
35375
|
+
["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.userId()` | `UserId` | An app user ID returned by `ctx.id`. See [Authentication](authentication.md). |\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\nEnvironment declarations also provide `values.secret()` and `values.secret(value)`. These produce\nredacted values that require `Secret.value(...)` for explicit access. Secret validators are not\navailable in schemas or function input and output contracts. See\n[Environment variables](environment-variables.md).\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\nMutation arguments accept objects with ordinary fields and arrays. Convert built-in collections\nsuch as `Map` and `Set` to objects or arrays, and convert `URL` instances to strings before calling\na mutation. Ignotum rejects these built-in objects instead of discarding their contents.\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"],
|
|
34954
35376
|
["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"],
|
|
34955
35377
|
["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"]
|
|
34956
35378
|
];
|