ignotum 0.0.11 → 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/README.md +8 -3
- package/dist/cli/bin.mjs +1372 -492
- 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-DR_8vfKg.d.ts → api-Cv3hMbzo.d.ts} +4 -3
- package/dist/runtime/client.d.ts +39 -8
- package/dist/runtime/client.js +505 -131
- 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-hmYFyqTa.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +13 -13
- package/dist/runtime/id-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/{index-2q9FwJud.d.ts → index-D54flWtH.d.ts} +136 -76
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +21 -17
- package/dist/runtime/internal/host.js +22 -8
- 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-Bt3l7QaC-BYEGmLBE.d.ts → pagination-DnKg3dkI-r5ZUxBBx.d.ts} +34 -6
- package/dist/runtime/pagination-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
- package/dist/runtime/{schema-ERFjT8-m.js → schema-1Zs03-iS.js} +135 -17
- package/dist/runtime/schema-1Zs03-iS.js.map +1 -0
- package/dist/runtime/server.d.ts +9 -3
- package/dist/runtime/server.js +5 -3
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-a2EdGSdY.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
- package/package.json +4 -4
- package/src/cli/agent-files.ts +8 -0
- package/src/cli/auth-client.ts +1 -1
- package/src/cli/bin.ts +13 -3
- package/src/cli/build/server.ts +102 -13
- package/src/cli/codegen.ts +7 -4
- package/src/cli/control-client.ts +30 -8
- package/src/cli/deploy.ts +7 -2
- package/src/cli/environment.ts +133 -0
- package/src/client/files.ts +43 -27
- package/src/client/hooks.ts +94 -17
- package/src/client/id.ts +259 -0
- package/src/client/index.ts +5 -2
- package/src/client/page-observers.ts +77 -0
- package/src/client/sync.ts +214 -69
- package/src/dev-runtime/functions.ts +232 -86
- package/src/dev-runtime/id.ts +174 -2
- package/src/dev-runtime/query-cache.ts +105 -0
- package/src/dev-runtime/sync.ts +187 -26
- package/src/server/index.ts +13 -1
- package/dist/runtime/api-DzcR7spt.js.map +0 -1
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js +0 -311
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.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-CFJ3xlAt.d.ts +0 -1
- package/dist/runtime/schema-ERFjT8-m.js.map +0 -1
package/dist/cli/bin.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire, isBuiltin } from "node:module";
|
|
3
|
+
import * as NFS from "node:fs";
|
|
4
|
+
import { writeSync } from "node:fs";
|
|
3
5
|
import process$1 from "node:process";
|
|
4
|
-
import * as Deferred from "effect/Deferred";
|
|
6
|
+
import * as Deferred$1 from "effect/Deferred";
|
|
5
7
|
import * as Effect$1 from "effect/Effect";
|
|
6
8
|
import { effectify } from "effect/Effect";
|
|
7
|
-
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as Exit$1 from "effect/Exit";
|
|
8
10
|
import * as FileSystem$1 from "effect/FileSystem";
|
|
9
11
|
import * as Layer$1 from "effect/Layer";
|
|
10
12
|
import * as Path$3 from "effect/Path";
|
|
@@ -35,7 +37,6 @@ import { isIP } from "node:net";
|
|
|
35
37
|
import * as EffectCrypto from "effect/Crypto";
|
|
36
38
|
import * as NodeCrypto from "node:crypto";
|
|
37
39
|
import * as Queue from "effect/Queue";
|
|
38
|
-
import * as NFS from "node:fs";
|
|
39
40
|
import * as OS from "node:os";
|
|
40
41
|
import { homedir } from "node:os";
|
|
41
42
|
import * as Path$2 from "node:path";
|
|
@@ -70,7 +71,7 @@ import * as RcRef from "effect/RcRef";
|
|
|
70
71
|
import * as Terminal$1 from "effect/Terminal";
|
|
71
72
|
import * as readline from "node:readline";
|
|
72
73
|
import * as Runtime from "effect/Runtime";
|
|
73
|
-
import { Array as Array$1, Brand, Cause, Config, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Redacted, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, 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";
|
|
74
75
|
import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
75
76
|
import { customAlphabet } from "nanoid";
|
|
76
77
|
import { createAuthClient } from "better-auth/client";
|
|
@@ -370,11 +371,11 @@ const readableToPullUnsafe = (options) => {
|
|
|
370
371
|
latch.openUnsafe();
|
|
371
372
|
}
|
|
372
373
|
function onError(error) {
|
|
373
|
-
exit.current = Exit.fail(options.onError(error));
|
|
374
|
+
exit.current = Exit$1.fail(options.onError(error));
|
|
374
375
|
latch.openUnsafe();
|
|
375
376
|
}
|
|
376
377
|
function onEnd() {
|
|
377
|
-
exit.current = Exit.fail(Cause$1.Done());
|
|
378
|
+
exit.current = Exit$1.fail(Cause$1.Done());
|
|
378
379
|
latch.openUnsafe();
|
|
379
380
|
}
|
|
380
381
|
readable.on("readable", onReadable);
|
|
@@ -419,7 +420,7 @@ var StreamAdapter = class extends Readable {
|
|
|
419
420
|
}))).pipe(this.readLatch.whenOpen, Effect$1.provideContext(context), Effect$1.runFork);
|
|
420
421
|
this.fiber.addObserver((exit) => {
|
|
421
422
|
this.fiber = void 0;
|
|
422
|
-
if (Exit.isSuccess(exit)) this.push(null);
|
|
423
|
+
if (Exit$1.isSuccess(exit)) this.push(null);
|
|
423
424
|
else this.destroy(Cause$1.squash(exit.cause));
|
|
424
425
|
});
|
|
425
426
|
}
|
|
@@ -601,13 +602,13 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
601
602
|
};
|
|
602
603
|
};
|
|
603
604
|
const spawn = (command, spawnOptions) => Effect$1.callback((resume) => {
|
|
604
|
-
const deferred = Deferred.makeUnsafe();
|
|
605
|
+
const deferred = Deferred$1.makeUnsafe();
|
|
605
606
|
const handle = NodeChildProcess.spawn(command.command, command.args, spawnOptions);
|
|
606
607
|
handle.on("error", (error) => {
|
|
607
608
|
resume(Effect$1.fail(toPlatformError("spawn", error, command)));
|
|
608
609
|
});
|
|
609
610
|
handle.on("exit", (...args) => {
|
|
610
|
-
Deferred.doneUnsafe(deferred, Exit.succeed(args));
|
|
611
|
+
Deferred$1.doneUnsafe(deferred, Exit$1.succeed(args));
|
|
611
612
|
});
|
|
612
613
|
handle.on("spawn", () => {
|
|
613
614
|
resume(Effect$1.succeed([handle, deferred]));
|
|
@@ -686,15 +687,15 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
686
687
|
env,
|
|
687
688
|
stdio
|
|
688
689
|
}, process.platform)), Effect$1.fnUntraced(function* ([childProcess, exitSignal]) {
|
|
689
|
-
const exited = yield* Deferred.isDone(exitSignal);
|
|
690
|
+
const exited = yield* Deferred$1.isDone(exitSignal);
|
|
690
691
|
const killWithTimeout = withTimeout(childProcess, cmd, cmd.options);
|
|
691
692
|
if (exited) {
|
|
692
|
-
const [code] = yield* Deferred.await(exitSignal);
|
|
693
|
+
const [code] = yield* Deferred$1.await(exitSignal);
|
|
693
694
|
if (code !== 0 && Predicate$1.isNotNull(code)) return yield* Effect$1.ignore(killWithTimeout(killProcessGroup));
|
|
694
695
|
return yield* Effect$1.void;
|
|
695
696
|
}
|
|
696
697
|
if (!isReferenced) return yield* Effect$1.void;
|
|
697
|
-
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);
|
|
698
699
|
}));
|
|
699
700
|
const pid = ProcessId(childProcess.pid);
|
|
700
701
|
childProcess.on("exit", (code) => {
|
|
@@ -716,14 +717,14 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
|
|
|
716
717
|
const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig);
|
|
717
718
|
const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig);
|
|
718
719
|
const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds);
|
|
719
|
-
const isRunning = Effect$1.map(Deferred.isDone(exitSignal), (done) => !done);
|
|
720
|
-
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]) => {
|
|
721
722
|
if (Predicate$1.isNotNull(code)) return Effect$1.succeed(ExitCode(code));
|
|
722
723
|
const error = new globalThis.Error(`Process interrupted due to receipt of signal: '${signal}'`);
|
|
723
724
|
return Effect$1.fail(toPlatformError("exitCode", error, cmd));
|
|
724
725
|
});
|
|
725
726
|
const kill = (options) => {
|
|
726
|
-
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);
|
|
727
728
|
};
|
|
728
729
|
return makeHandle({
|
|
729
730
|
pid,
|
|
@@ -29412,11 +29413,32 @@ defineId("conn", "ignotum/gateway/ConnectionId");
|
|
|
29412
29413
|
const RuntimeRequestNonce = defineId("nonce", "ignotum/runtime/RequestNonce");
|
|
29413
29414
|
const RequestId = defineId("req", "ignotum/runtime/RequestId");
|
|
29414
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");
|
|
29415
29421
|
//#endregion
|
|
29416
29422
|
//#region ../contracts/dist/runtime/identity.js
|
|
29417
29423
|
const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
|
|
29418
29424
|
const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
|
|
29419
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
|
+
};
|
|
29420
29442
|
const FileFormat = Schema.Literals([
|
|
29421
29443
|
"jpeg",
|
|
29422
29444
|
"png",
|
|
@@ -29570,6 +29592,158 @@ const datePathsOf = (value) => {
|
|
|
29570
29592
|
};
|
|
29571
29593
|
const datePathsOfObject = (value) => datePathsOf(Schema.decodeUnknownSync(TransportObjectTypeSchema)(value));
|
|
29572
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
|
|
29573
29747
|
//#region ../contracts/dist/runtime/sync.js
|
|
29574
29748
|
const FunctionNamePart = Schema.String.check(Schema.isPattern(/^[A-Za-z_$][A-Za-z0-9_$]*$/));
|
|
29575
29749
|
const ApiFunctionAddressParts = Schema.TemplateLiteralParser([
|
|
@@ -29645,12 +29819,19 @@ const WireFailure = Schema.Struct({
|
|
|
29645
29819
|
});
|
|
29646
29820
|
const WireResult = Schema.Union([WireSuccess, WireFailure]);
|
|
29647
29821
|
const SyncHandshake = Schema.Struct({
|
|
29822
|
+
session: Schema.optional(SessionState),
|
|
29823
|
+
serverTime: Schema.optional(Schema.Finite),
|
|
29648
29824
|
type: Schema.Literal("Handshake"),
|
|
29649
29825
|
appId: AppId,
|
|
29650
29826
|
deploymentId: DeploymentId,
|
|
29651
29827
|
generation: DeploymentGeneration
|
|
29652
29828
|
});
|
|
29653
29829
|
const Snapshot = Schema.Struct({
|
|
29830
|
+
identity: Schema.optional(Schema.Struct({
|
|
29831
|
+
sessionEpoch: Schema.String,
|
|
29832
|
+
viewRevision: Schema.Natural,
|
|
29833
|
+
dependsOnId: Schema.Boolean
|
|
29834
|
+
})),
|
|
29654
29835
|
type: Schema.Literal("Snapshot"),
|
|
29655
29836
|
id: SubscriptionId,
|
|
29656
29837
|
result: WireResult,
|
|
@@ -29661,6 +29842,7 @@ const Snapshot = Schema.Struct({
|
|
|
29661
29842
|
})))
|
|
29662
29843
|
});
|
|
29663
29844
|
const Preparation = Schema.Struct({
|
|
29845
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29664
29846
|
type: Schema.Literal("Preparation"),
|
|
29665
29847
|
id: InvocationId,
|
|
29666
29848
|
kind: Schema.Literal("Mutation"),
|
|
@@ -29670,17 +29852,21 @@ const Preparation = Schema.Struct({
|
|
|
29670
29852
|
}))
|
|
29671
29853
|
});
|
|
29672
29854
|
const SyncResultSuccess = Schema.Struct({
|
|
29855
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29673
29856
|
type: Schema.Literal("Result"),
|
|
29674
29857
|
id: InvocationId,
|
|
29675
29858
|
result: WireSuccess,
|
|
29676
29859
|
committedRevision: AppStateRevision
|
|
29677
29860
|
});
|
|
29678
29861
|
const SyncResultFailure = Schema.Struct({
|
|
29862
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29679
29863
|
type: Schema.Literal("Result"),
|
|
29680
29864
|
id: InvocationId,
|
|
29681
29865
|
result: WireFailure
|
|
29682
29866
|
});
|
|
29683
29867
|
const ErrorCode = Schema.Literals([
|
|
29868
|
+
"SessionChanged",
|
|
29869
|
+
"SessionExpired",
|
|
29684
29870
|
"DuplicateOperationId",
|
|
29685
29871
|
"FunctionUnavailable",
|
|
29686
29872
|
"InvalidArguments",
|
|
@@ -29691,6 +29877,7 @@ const ErrorCode = Schema.Literals([
|
|
|
29691
29877
|
"WrongFunctionKind"
|
|
29692
29878
|
]);
|
|
29693
29879
|
const SyncError = Schema.Struct({
|
|
29880
|
+
sessionEpoch: Schema.optional(Schema.String),
|
|
29694
29881
|
type: Schema.Literal("Error"),
|
|
29695
29882
|
operation: Schema.optional(Operation),
|
|
29696
29883
|
code: ErrorCode,
|
|
@@ -29701,7 +29888,30 @@ const Deployment = Schema.Struct({
|
|
|
29701
29888
|
deploymentId: DeploymentId,
|
|
29702
29889
|
generation: DeploymentGeneration
|
|
29703
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
|
+
});
|
|
29704
29912
|
const ServerMessage = Schema.Union([
|
|
29913
|
+
Session,
|
|
29914
|
+
Invalidate,
|
|
29705
29915
|
SyncHandshake,
|
|
29706
29916
|
Snapshot,
|
|
29707
29917
|
Preparation,
|
|
@@ -29735,7 +29945,7 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
|
|
|
29735
29945
|
message: Schema.String
|
|
29736
29946
|
}) {};
|
|
29737
29947
|
//#endregion
|
|
29738
|
-
//#region ../contracts/dist/descriptor-
|
|
29948
|
+
//#region ../contracts/dist/descriptor-Cyo9FP9n.js
|
|
29739
29949
|
const LiteralValue = Schema.Union([
|
|
29740
29950
|
Schema.String,
|
|
29741
29951
|
Schema.Finite,
|
|
@@ -29772,6 +29982,7 @@ const ValueDescriptor = Schema.Union([
|
|
|
29772
29982
|
type: Schema.Literal("id"),
|
|
29773
29983
|
table: Schema.String
|
|
29774
29984
|
}),
|
|
29985
|
+
Schema.Struct({ type: Schema.Literal("userId") }),
|
|
29775
29986
|
Schema.Struct({ type: Schema.Literal("integer") }),
|
|
29776
29987
|
Schema.Struct({
|
|
29777
29988
|
type: Schema.Literal("literal"),
|
|
@@ -29800,6 +30011,10 @@ const ValueDescriptor = Schema.Union([
|
|
|
29800
30011
|
type: Schema.Literal("record"),
|
|
29801
30012
|
value: Schema.suspend(() => ValueDescriptor)
|
|
29802
30013
|
}),
|
|
30014
|
+
Schema.Struct({
|
|
30015
|
+
type: Schema.Literal("secret"),
|
|
30016
|
+
value: Schema.suspend(() => ValueDescriptor)
|
|
30017
|
+
}),
|
|
29803
30018
|
Schema.Struct({ type: Schema.Literal("string") }),
|
|
29804
30019
|
Schema.Struct({
|
|
29805
30020
|
type: Schema.Literal("union"),
|
|
@@ -29807,7 +30022,7 @@ const ValueDescriptor = Schema.Union([
|
|
|
29807
30022
|
})
|
|
29808
30023
|
]);
|
|
29809
30024
|
const ValueDescriptorTypeId = Symbol.for("ignotum/schema/ValueDescriptor");
|
|
29810
|
-
Function$1.dual(2, (value, descriptor) => {
|
|
30025
|
+
const attachValueDescriptor = Function$1.dual(2, (value, descriptor) => {
|
|
29811
30026
|
Object.defineProperty(value, ValueDescriptorTypeId, {
|
|
29812
30027
|
configurable: false,
|
|
29813
30028
|
enumerable: false,
|
|
@@ -29826,13 +30041,30 @@ const descriptorContainsFile = (descriptor) => {
|
|
|
29826
30041
|
case "array":
|
|
29827
30042
|
case "nullable":
|
|
29828
30043
|
case "optional":
|
|
29829
|
-
case "record":
|
|
30044
|
+
case "record":
|
|
30045
|
+
case "secret": return descriptorContainsFile(descriptor.value);
|
|
29830
30046
|
case "error":
|
|
29831
30047
|
case "object": return descriptor.fields.some((field) => descriptorContainsFile(field.value));
|
|
29832
30048
|
case "union": return descriptor.members.some(descriptorContainsFile);
|
|
29833
30049
|
default: return false;
|
|
29834
30050
|
}
|
|
29835
30051
|
};
|
|
30052
|
+
const descriptorAllowedInEnvironment = (descriptor) => {
|
|
30053
|
+
switch (descriptor.type) {
|
|
30054
|
+
case "error":
|
|
30055
|
+
case "file":
|
|
30056
|
+
case "userId":
|
|
30057
|
+
case "id": return false;
|
|
30058
|
+
case "array":
|
|
30059
|
+
case "nullable":
|
|
30060
|
+
case "optional":
|
|
30061
|
+
case "record":
|
|
30062
|
+
case "secret": return descriptorAllowedInEnvironment(descriptor.value);
|
|
30063
|
+
case "object": return descriptor.fields.every((field) => descriptorAllowedInEnvironment(field.value));
|
|
30064
|
+
case "union": return descriptor.members.every(descriptorAllowedInEnvironment);
|
|
30065
|
+
default: return true;
|
|
30066
|
+
}
|
|
30067
|
+
};
|
|
29836
30068
|
const descriptorFields = (fields) => {
|
|
29837
30069
|
const entries = [];
|
|
29838
30070
|
for (const name of Reflect.ownKeys(fields)) {
|
|
@@ -29848,22 +30080,6 @@ const descriptorFields = (fields) => {
|
|
|
29848
30080
|
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
29849
30081
|
};
|
|
29850
30082
|
//#endregion
|
|
29851
|
-
//#region ../contracts/dist/versioned.js
|
|
29852
|
-
/** Registers the first revision of a long-lived format. */
|
|
29853
|
-
const initial = (schema) => schema;
|
|
29854
|
-
/** Adds one adjacent migration and keeps encoding on the new current revision. */
|
|
29855
|
-
const upgrade = (previous, current, migrate) => {
|
|
29856
|
-
const accepted = Schema.Union([previous, current]);
|
|
29857
|
-
const isCurrent = Schema.is(current);
|
|
29858
|
-
return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
|
|
29859
|
-
decode: SchemaGetter.transform((value) => {
|
|
29860
|
-
if (isCurrent(value)) return value;
|
|
29861
|
-
return migrate(value);
|
|
29862
|
-
}),
|
|
29863
|
-
encode: SchemaGetter.transform((value) => value)
|
|
29864
|
-
}));
|
|
29865
|
-
};
|
|
29866
|
-
//#endregion
|
|
29867
30083
|
//#region ../contracts/dist/deployment.js
|
|
29868
30084
|
const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
|
|
29869
30085
|
const ClientPath = Schema.String.check(Schema.isPattern(/^\/(?!_ignotum(?:\/|$))(?:(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+\/?)?$/)).pipe(Schema.brand("ignotum/deployment/ClientPath"));
|
|
@@ -29876,6 +30092,7 @@ const ArtifactKind = Schema.Literals([
|
|
|
29876
30092
|
"ClientShell",
|
|
29877
30093
|
"FunctionBundle",
|
|
29878
30094
|
"ServerManifest",
|
|
30095
|
+
"ServerEnvironment",
|
|
29879
30096
|
"SourceMap",
|
|
29880
30097
|
"SchemaSnapshot"
|
|
29881
30098
|
]);
|
|
@@ -29896,9 +30113,17 @@ const ClientRoute = Schema.Struct({
|
|
|
29896
30113
|
pathname: ClientPath,
|
|
29897
30114
|
artifact: ArtifactReference
|
|
29898
30115
|
});
|
|
29899
|
-
const
|
|
30116
|
+
const DeploymentInventoryV1 = Schema.Struct({
|
|
29900
30117
|
formatVersion: Schema.Literal(1),
|
|
29901
30118
|
files: Schema.Array(ArtifactFile)
|
|
30119
|
+
});
|
|
30120
|
+
const DeploymentInventoryV2 = Schema.Struct({
|
|
30121
|
+
formatVersion: Schema.Literal(2),
|
|
30122
|
+
files: Schema.Array(ArtifactFile)
|
|
30123
|
+
});
|
|
30124
|
+
const DeploymentInventory = upgrade(initial(DeploymentInventoryV1), DeploymentInventoryV2, (inventory) => ({
|
|
30125
|
+
formatVersion: 2,
|
|
30126
|
+
files: inventory.files
|
|
29902
30127
|
}));
|
|
29903
30128
|
const ClientManifest = initial(Schema.Struct({
|
|
29904
30129
|
formatVersion: Schema.Literal(1),
|
|
@@ -29918,6 +30143,15 @@ const SchemaSnapshotField = Schema.Struct({
|
|
|
29918
30143
|
name: Schema.String,
|
|
29919
30144
|
value: ValueDescriptor
|
|
29920
30145
|
});
|
|
30146
|
+
const EnvironmentArtifact = initial(Schema.Struct({
|
|
30147
|
+
formatVersion: Schema.Literal(1),
|
|
30148
|
+
nonce: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)),
|
|
30149
|
+
values: Schema.Record(Schema.String, Schema.String)
|
|
30150
|
+
}));
|
|
30151
|
+
const ServerEnvironmentArtifact = Schema.Struct({
|
|
30152
|
+
artifact: ArtifactReference,
|
|
30153
|
+
fields: Schema.Array(SchemaSnapshotField)
|
|
30154
|
+
});
|
|
29921
30155
|
const SchemaSnapshotIndex = Schema.Struct({
|
|
29922
30156
|
name: Schema.String,
|
|
29923
30157
|
fields: Schema.Array(Schema.String)
|
|
@@ -29971,10 +30205,19 @@ const ServerBuildManifestV2 = Schema.Struct({
|
|
|
29971
30205
|
schema: ArtifactReference,
|
|
29972
30206
|
functions: Schema.Array(ServerFunctionArtifact)
|
|
29973
30207
|
});
|
|
29974
|
-
const ServerBuildManifest = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
|
|
30208
|
+
const ServerBuildManifest = upgrade(upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
|
|
29975
30209
|
formatVersion: 2,
|
|
29976
30210
|
schema: manifest.schema,
|
|
29977
30211
|
functions: manifest.functions
|
|
30212
|
+
})), Schema.Struct({
|
|
30213
|
+
formatVersion: Schema.Literal(3),
|
|
30214
|
+
schema: ArtifactReference,
|
|
30215
|
+
environment: Schema.optional(ServerEnvironmentArtifact),
|
|
30216
|
+
functions: Schema.Array(ServerFunctionArtifact)
|
|
30217
|
+
}), (manifest) => ({
|
|
30218
|
+
formatVersion: 3,
|
|
30219
|
+
schema: manifest.schema,
|
|
30220
|
+
functions: manifest.functions
|
|
29978
30221
|
}));
|
|
29979
30222
|
const deploymentInventoryPath = ArtifactPath.make("inventory.json");
|
|
29980
30223
|
const clientManifestPath = ArtifactPath.make("client/manifest.json");
|
|
@@ -29993,6 +30236,7 @@ const clientPublicFileExtensions = [
|
|
|
29993
30236
|
];
|
|
29994
30237
|
const serverManifestPath = ArtifactPath.make("server/manifest.json");
|
|
29995
30238
|
const schemaSnapshotPath = ArtifactPath.make("server/schema.json");
|
|
30239
|
+
const serverEnvironmentPath = ArtifactPath.make("server/environment.json");
|
|
29996
30240
|
const deploymentArtifactLimits = {
|
|
29997
30241
|
fileBytes: 16777216,
|
|
29998
30242
|
fileCount: 512,
|
|
@@ -30091,7 +30335,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
|
|
|
30091
30335
|
};
|
|
30092
30336
|
//#endregion
|
|
30093
30337
|
//#region package.json
|
|
30094
|
-
var version = "0.0.
|
|
30338
|
+
var version = "0.0.13";
|
|
30095
30339
|
//#endregion
|
|
30096
30340
|
//#region src/cli/codegen.ts
|
|
30097
30341
|
const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
|
|
@@ -30107,7 +30351,7 @@ var GeneratedFileConflict = class extends Schema.TaggedError()("GeneratedFileCon
|
|
|
30107
30351
|
message: Schema.String,
|
|
30108
30352
|
path: Schema.String
|
|
30109
30353
|
}) {};
|
|
30110
|
-
const isFunctionModuleFile = (fileName) => fileName.endsWith(".ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".spec.ts") && fileName !== "index.ts" && fileName !== "schema.ts" && !fileName.startsWith("_");
|
|
30354
|
+
const isFunctionModuleFile = (fileName) => fileName.endsWith(".ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".spec.ts") && fileName !== "index.ts" && fileName !== "env.ts" && fileName !== "schema.ts" && !fileName.startsWith("_");
|
|
30111
30355
|
const moduleNameFromFile = (filePath, fileName) => {
|
|
30112
30356
|
const moduleName = fileName.slice(0, -3);
|
|
30113
30357
|
return Schema.decodeEffect(FunctionNamePart)(moduleName).pipe(Effect.mapError(() => InvalidFunctionModuleName.make({
|
|
@@ -30115,13 +30359,13 @@ const moduleNameFromFile = (filePath, fileName) => {
|
|
|
30115
30359
|
path: filePath
|
|
30116
30360
|
})));
|
|
30117
30361
|
};
|
|
30118
|
-
const renderServerBindings = () => `${generatedHeader}
|
|
30362
|
+
const renderServerBindings = (hasEnvironment) => `${generatedHeader}
|
|
30119
30363
|
|
|
30120
30364
|
import { bindSchema } from "ignotum/internal/server";
|
|
30121
30365
|
|
|
30122
|
-
import schema from "../server/schema.js";
|
|
30366
|
+
${hasEnvironment ? "import environment from \"../server/env.js\";\n" : ""}import schema from "../server/schema.js";
|
|
30123
30367
|
|
|
30124
|
-
export const { mutation, query, values } = bindSchema(schema);
|
|
30368
|
+
export const { mutation, query, values } = bindSchema(schema${hasEnvironment ? ", environment" : ""});
|
|
30125
30369
|
`;
|
|
30126
30370
|
const renderTypes = () => `${generatedHeader}
|
|
30127
30371
|
|
|
@@ -30186,16 +30430,18 @@ const generate = Effect.fn("Codegen.generate")(function* (appDirectory) {
|
|
|
30186
30430
|
const path = yield* Path$1.Path;
|
|
30187
30431
|
const serverDirectory = path.join(appDirectory, "server");
|
|
30188
30432
|
const schemaPath = path.join(serverDirectory, "schema.ts");
|
|
30433
|
+
const environmentPath = path.join(serverDirectory, "env.ts");
|
|
30189
30434
|
if (!(yield* fileSystem.exists(schemaPath))) return yield* SchemaNotFound.make({
|
|
30190
30435
|
message: `No Ignotum schema found at ${schemaPath}.`,
|
|
30191
30436
|
path: schemaPath
|
|
30192
30437
|
});
|
|
30193
30438
|
const entries = yield* fileSystem.readDirectory(serverDirectory);
|
|
30439
|
+
const hasEnvironment = yield* fileSystem.exists(environmentPath);
|
|
30194
30440
|
const functionFiles = Array$1.sort(String$1.Order)(Array$1.filter(entries, isFunctionModuleFile));
|
|
30195
30441
|
const functionModules = yield* Effect.forEach(functionFiles, (fileName) => moduleNameFromFile(path.join(serverDirectory, fileName), fileName));
|
|
30196
30442
|
const outputs = [
|
|
30197
30443
|
{
|
|
30198
|
-
content: renderServerBindings(),
|
|
30444
|
+
content: renderServerBindings(hasEnvironment),
|
|
30199
30445
|
path: path.join(appDirectory, "_generated", "server.ts")
|
|
30200
30446
|
},
|
|
30201
30447
|
{
|
|
@@ -30226,7 +30472,7 @@ const Credential = initial(Schema.Struct({
|
|
|
30226
30472
|
}));
|
|
30227
30473
|
const AuthUrl = Schema.URL.check(Schema.makeFilter((url) => url.protocol === "https:" || url.protocol === "http:" && [
|
|
30228
30474
|
"127.0.0.1",
|
|
30229
|
-
"::1",
|
|
30475
|
+
"[::1]",
|
|
30230
30476
|
"localhost"
|
|
30231
30477
|
].includes(url.hostname), { message: "The auth URL must use HTTPS unless it targets localhost." }));
|
|
30232
30478
|
var AuthCommandError = class extends Schema.TaggedError()("AuthCommandError", {
|
|
@@ -30439,7 +30685,7 @@ const uploadFile = (file, sourcePath) => ({
|
|
|
30439
30685
|
size: file.size,
|
|
30440
30686
|
sourcePath
|
|
30441
30687
|
});
|
|
30442
|
-
const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFiles")(function* (inventory, inventorySha256, inventorySize, buildDirectory) {
|
|
30688
|
+
const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFiles")(function* (inventory, inventorySha256, inventorySize, buildDirectory, memoryFiles = /* @__PURE__ */ new Map()) {
|
|
30443
30689
|
const path = yield* Path$1.Path;
|
|
30444
30690
|
return [{
|
|
30445
30691
|
contentType: "application/json; charset=utf-8",
|
|
@@ -30447,7 +30693,16 @@ const deploymentUploadFiles = Effect.fn("ControlClient.deploymentUploadFiles")(f
|
|
|
30447
30693
|
sha256: inventorySha256,
|
|
30448
30694
|
size: inventorySize,
|
|
30449
30695
|
sourcePath: path.join(buildDirectory, deploymentInventoryPath)
|
|
30450
|
-
}, ...inventory.files.map((file) =>
|
|
30696
|
+
}, ...inventory.files.map((file) => {
|
|
30697
|
+
const bytes = memoryFiles.get(file.path);
|
|
30698
|
+
return bytes === void 0 ? uploadFile(file, path.join(buildDirectory, file.path)) : {
|
|
30699
|
+
bytes,
|
|
30700
|
+
contentType: file.contentType,
|
|
30701
|
+
path: file.path,
|
|
30702
|
+
sha256: file.sha256,
|
|
30703
|
+
size: file.size
|
|
30704
|
+
};
|
|
30705
|
+
})];
|
|
30451
30706
|
});
|
|
30452
30707
|
const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
30453
30708
|
const config = yield* HostedControlConfiguration;
|
|
@@ -30497,7 +30752,8 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
|
30497
30752
|
return yield* execute(request, ControlDeployment);
|
|
30498
30753
|
});
|
|
30499
30754
|
const uploadDeploymentFile = Effect.fn("ControlClient.uploadDeploymentFile")(function* (appId, deploymentId, file) {
|
|
30500
|
-
const
|
|
30755
|
+
const baseRequest = HttpClientRequest.put(endpoint(config.apiUrl, controlDeploymentFilePath(appId, deploymentId, file.path))).pipe(HttpClientRequest.setHeader(controlUploadHeaderNames.contentLength, String(file.size)), HttpClientRequest.setHeader(controlUploadHeaderNames.sha256, file.sha256));
|
|
30756
|
+
const request = yield* file.bytes === void 0 ? file.sourcePath === void 0 ? Effect.fail(unavailable(`Deployment file '${file.path}' has no upload source.`, {})) : HttpClientRequest.bodyFile(baseRequest, file.sourcePath, { contentType: file.contentType }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.mapError((cause) => unavailable(`Deployment file '${file.path}' could not be opened.`, cause))) : Effect.succeed(HttpClientRequest.bodyUint8Array(baseRequest, file.bytes, file.contentType));
|
|
30501
30757
|
return (yield* executeIdempotent(request, DeploymentFileUploadResponse)).status;
|
|
30502
30758
|
});
|
|
30503
30759
|
const finalizeDeployment = Effect.fn("ControlClient.finalizeDeployment")(function* (appId, deploymentId) {
|
|
@@ -30517,95 +30773,6 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
|
|
|
30517
30773
|
});
|
|
30518
30774
|
}));
|
|
30519
30775
|
//#endregion
|
|
30520
|
-
//#region ../contracts/dist/json.js
|
|
30521
|
-
const encodeScalar$1 = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
|
|
30522
|
-
const encodeCanonicalJson = (value) => {
|
|
30523
|
-
if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar$1(value);
|
|
30524
|
-
if (Predicate.isObject(value)) return `{${Array$1.map(Array$1.sort(String$1.Order)(Object.keys(value)), (key) => {
|
|
30525
|
-
const field = Schema.decodeUnknownSync(Schema.Json)(value[key]);
|
|
30526
|
-
return `${encodeScalar$1(key)}:${encodeCanonicalJson(field)}`;
|
|
30527
|
-
}).join(",")}}`;
|
|
30528
|
-
return `[${Schema.decodeUnknownSync(Schema.Array(Schema.Json))(value).map(encodeCanonicalJson).join(",")}]`;
|
|
30529
|
-
};
|
|
30530
|
-
//#endregion
|
|
30531
|
-
//#region ../contracts/dist/runtime/result.js
|
|
30532
|
-
const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
|
|
30533
|
-
const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
|
|
30534
|
-
const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
|
|
30535
|
-
const ErrorBrand = "ignotum/error";
|
|
30536
|
-
Brand.nominal();
|
|
30537
|
-
const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
|
|
30538
|
-
var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: RequestId }) {};
|
|
30539
|
-
const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
|
|
30540
|
-
const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
|
|
30541
|
-
function makeResultEffectBase() {
|
|
30542
|
-
const Base = function() {};
|
|
30543
|
-
Base.prototype = Effectable.Prototype({
|
|
30544
|
-
label: "IgnotumResult",
|
|
30545
|
-
evaluate() {
|
|
30546
|
-
return this[ResultTypeId];
|
|
30547
|
-
}
|
|
30548
|
-
});
|
|
30549
|
-
return Base;
|
|
30550
|
-
}
|
|
30551
|
-
const ResultEffectBase = makeResultEffectBase();
|
|
30552
|
-
var ResultOperation = class ResultOperation extends ResultEffectBase {
|
|
30553
|
-
[ResultTypeId];
|
|
30554
|
-
constructor(effect, completed) {
|
|
30555
|
-
super();
|
|
30556
|
-
this[ResultTypeId] = effect;
|
|
30557
|
-
if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
|
|
30558
|
-
}
|
|
30559
|
-
catch(cases) {
|
|
30560
|
-
const effectCases = {};
|
|
30561
|
-
for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
|
|
30562
|
-
const runtimeEffect = this[ResultTypeId];
|
|
30563
|
-
const caught = Effect.catchTags(runtimeEffect, effectCases);
|
|
30564
|
-
return new ResultOperation(caught);
|
|
30565
|
-
}
|
|
30566
|
-
};
|
|
30567
|
-
var PendingQueryResult = class {
|
|
30568
|
-
[PendingResultTypeId] = PendingResultTypeId;
|
|
30569
|
-
};
|
|
30570
|
-
const inspectResult = (value) => {
|
|
30571
|
-
if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
|
|
30572
|
-
return value[CompletedResultTypeId];
|
|
30573
|
-
};
|
|
30574
|
-
const resultFromEffect = (effect) => new ResultOperation(effect);
|
|
30575
|
-
const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
|
|
30576
|
-
Function$1.dual(2, (result, matchers) => {
|
|
30577
|
-
if (result instanceof PendingQueryResult) {
|
|
30578
|
-
if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
|
|
30579
|
-
return matchers.pending();
|
|
30580
|
-
}
|
|
30581
|
-
if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
|
|
30582
|
-
const inspected = inspectResult(result);
|
|
30583
|
-
if (inspected?.type === "Success") {
|
|
30584
|
-
const value = inspected.value;
|
|
30585
|
-
return matchers.value(value);
|
|
30586
|
-
}
|
|
30587
|
-
if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
|
|
30588
|
-
const error = result[CompletedResultTypeId].error;
|
|
30589
|
-
if (error._tag === "InternalServerError") {
|
|
30590
|
-
const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
|
|
30591
|
-
const handler = matchers.internalError;
|
|
30592
|
-
if (handler === void 0) throw internalError;
|
|
30593
|
-
return handler(internalError);
|
|
30594
|
-
}
|
|
30595
|
-
const errorMatcher = matchers.error;
|
|
30596
|
-
if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
30597
|
-
if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
|
|
30598
|
-
const handlers = errorMatcher;
|
|
30599
|
-
const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
|
|
30600
|
-
if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
|
|
30601
|
-
return handler(error);
|
|
30602
|
-
});
|
|
30603
|
-
const documentNotFound = (table, id) => Brand.nominal()({
|
|
30604
|
-
_tag: "DocumentNotFound",
|
|
30605
|
-
table,
|
|
30606
|
-
id
|
|
30607
|
-
});
|
|
30608
|
-
//#endregion
|
|
30609
30776
|
//#region ../contracts/dist/runtime/pagination.js
|
|
30610
30777
|
const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
|
|
30611
30778
|
const PaginationPageSize = Schema.Int.check(Schema.isBetween({
|
|
@@ -30645,7 +30812,9 @@ const encodePaginationCursor = (query, position) => PaginationCursor.make(Encodi
|
|
|
30645
30812
|
position
|
|
30646
30813
|
})));
|
|
30647
30814
|
const decodePaginationCursor = (cursor) => Schema.decodeSync(CursorPayloadJson)(Result.getOrThrow(Encoding.decodeBase64UrlString(cursor)));
|
|
30648
|
-
|
|
30815
|
+
//#endregion
|
|
30816
|
+
//#region ../contracts/dist/schema/values.js
|
|
30817
|
+
const LiteralValueSchema = Schema.Union([
|
|
30649
30818
|
Schema.String,
|
|
30650
30819
|
Schema.Finite,
|
|
30651
30820
|
Schema.Boolean
|
|
@@ -30657,14 +30826,220 @@ const schemaIndexLimits = {
|
|
|
30657
30826
|
keyBytes: 4096,
|
|
30658
30827
|
nameBytes: 64
|
|
30659
30828
|
};
|
|
30829
|
+
const fresh = (value) => value.pipe(Schema.annotate({}));
|
|
30830
|
+
const string = () => attachValueDescriptor(fresh(Schema.String), { type: "string" });
|
|
30831
|
+
const number = () => attachValueDescriptor(fresh(Schema.Finite), { type: "number" });
|
|
30832
|
+
const integer = () => attachValueDescriptor(fresh(Schema.Int), { type: "integer" });
|
|
30833
|
+
const boolean = () => attachValueDescriptor(fresh(Schema.Boolean), { type: "boolean" });
|
|
30834
|
+
const date = () => attachValueDescriptor(fresh(Schema.DateFromMillis), { type: "date" });
|
|
30835
|
+
const never = () => attachValueDescriptor(fresh(Schema.Never), { type: "never" });
|
|
30836
|
+
const nullValue = () => attachValueDescriptor(fresh(Schema.Null), { type: "null" });
|
|
30837
|
+
const literal = (value) => {
|
|
30838
|
+
Schema.decodeSync(LiteralValueSchema)(value);
|
|
30839
|
+
return attachValueDescriptor(Schema.Literal(value), {
|
|
30840
|
+
type: "literal",
|
|
30841
|
+
value
|
|
30842
|
+
});
|
|
30843
|
+
};
|
|
30844
|
+
const literals = (...members) => {
|
|
30845
|
+
Schema.decodeSync(Schema.Array(LiteralValueSchema))(members);
|
|
30846
|
+
return attachValueDescriptor(Schema.Literals(members), {
|
|
30847
|
+
type: "literals",
|
|
30848
|
+
values: members
|
|
30849
|
+
});
|
|
30850
|
+
};
|
|
30851
|
+
const requireDescriptor = (value) => {
|
|
30852
|
+
const descriptor = getValueDescriptor(value);
|
|
30853
|
+
if (descriptor === void 0) throw new Error("Ignotum value combinators require a values validator.");
|
|
30854
|
+
return descriptor;
|
|
30855
|
+
};
|
|
30856
|
+
const array = (value) => {
|
|
30857
|
+
return attachValueDescriptor(Schema.Array(value), {
|
|
30858
|
+
type: "array",
|
|
30859
|
+
value: requireDescriptor(value)
|
|
30860
|
+
});
|
|
30861
|
+
};
|
|
30862
|
+
const assertFieldsExist = (fields, keys, operation) => {
|
|
30863
|
+
for (const key of keys) if (!Object.hasOwn(fields, key)) throw new Error(`Cannot ${operation} unknown field '${String(key)}'.`);
|
|
30864
|
+
};
|
|
30865
|
+
const fixedObject = (fields) => {
|
|
30866
|
+
const pick = (...keys) => {
|
|
30867
|
+
assertFieldsExist(fields, keys, "pick");
|
|
30868
|
+
return fixedObject(Struct.pick(fields, keys));
|
|
30869
|
+
};
|
|
30870
|
+
const omit = (...keys) => {
|
|
30871
|
+
assertFieldsExist(fields, keys, "omit");
|
|
30872
|
+
return fixedObject(Struct.omit(fields, keys));
|
|
30873
|
+
};
|
|
30874
|
+
const extend = (addedFields, ..._validation) => {
|
|
30875
|
+
for (const key of Reflect.ownKeys(addedFields)) if (Object.hasOwn(fields, key)) throw new Error(`Cannot extend existing field '${String(key)}'.`);
|
|
30876
|
+
return fixedObject(Struct.assign(fields, addedFields));
|
|
30877
|
+
};
|
|
30878
|
+
const override = (overriddenFields, ..._validation) => {
|
|
30879
|
+
assertFieldsExist(fields, Reflect.ownKeys(overriddenFields), "override");
|
|
30880
|
+
return fixedObject(Struct.assign(fields, overriddenFields));
|
|
30881
|
+
};
|
|
30882
|
+
const partial = () => {
|
|
30883
|
+
const partialFields = Struct.map(fields, Schema.optional);
|
|
30884
|
+
for (const name of Reflect.ownKeys(fields)) {
|
|
30885
|
+
const field = fields[name];
|
|
30886
|
+
const partialField = partialFields[name];
|
|
30887
|
+
if (field === void 0 || partialField === void 0) continue;
|
|
30888
|
+
attachValueDescriptor(partialField, {
|
|
30889
|
+
type: "optional",
|
|
30890
|
+
value: requireDescriptor(field)
|
|
30891
|
+
});
|
|
30892
|
+
}
|
|
30893
|
+
return fixedObject(partialFields);
|
|
30894
|
+
};
|
|
30895
|
+
const schema = attachValueDescriptor(Schema.Struct(fields), {
|
|
30896
|
+
type: "object",
|
|
30897
|
+
fields: descriptorFields(fields)
|
|
30898
|
+
});
|
|
30899
|
+
return Object.assign(schema, {
|
|
30900
|
+
extend,
|
|
30901
|
+
omit,
|
|
30902
|
+
override,
|
|
30903
|
+
partial,
|
|
30904
|
+
pick
|
|
30905
|
+
});
|
|
30906
|
+
};
|
|
30907
|
+
const object = (fields) => fixedObject(fields);
|
|
30908
|
+
const optional = (value) => {
|
|
30909
|
+
return attachValueDescriptor(Schema.optional(value), {
|
|
30910
|
+
type: "optional",
|
|
30911
|
+
value: requireDescriptor(value)
|
|
30912
|
+
});
|
|
30913
|
+
};
|
|
30914
|
+
const nullable = (value) => {
|
|
30915
|
+
return attachValueDescriptor(Schema.NullOr(value), {
|
|
30916
|
+
type: "nullable",
|
|
30917
|
+
value: requireDescriptor(value)
|
|
30918
|
+
});
|
|
30919
|
+
};
|
|
30920
|
+
const record = (value) => {
|
|
30921
|
+
return attachValueDescriptor(Schema.Record(Schema.String, value), {
|
|
30922
|
+
type: "record",
|
|
30923
|
+
value: requireDescriptor(value)
|
|
30924
|
+
});
|
|
30925
|
+
};
|
|
30926
|
+
const union = (...members) => {
|
|
30927
|
+
return attachValueDescriptor(Schema.Union(members), {
|
|
30928
|
+
type: "union",
|
|
30929
|
+
members: members.map(requireDescriptor)
|
|
30930
|
+
});
|
|
30931
|
+
};
|
|
30932
|
+
Redacted.isRedacted, Redacted.value;
|
|
30933
|
+
function secret(value = string()) {
|
|
30934
|
+
return attachValueDescriptor(Schema.RedactedFromValue(value, { disallowEncode: true }), {
|
|
30935
|
+
type: "secret",
|
|
30936
|
+
value: requireDescriptor(value)
|
|
30937
|
+
});
|
|
30938
|
+
}
|
|
30939
|
+
const environmentValues = {
|
|
30940
|
+
array,
|
|
30941
|
+
boolean,
|
|
30942
|
+
date,
|
|
30943
|
+
integer,
|
|
30944
|
+
literal,
|
|
30945
|
+
literals,
|
|
30946
|
+
never,
|
|
30947
|
+
null: nullValue,
|
|
30948
|
+
nullable,
|
|
30949
|
+
number,
|
|
30950
|
+
object,
|
|
30951
|
+
optional,
|
|
30952
|
+
record,
|
|
30953
|
+
secret,
|
|
30954
|
+
string,
|
|
30955
|
+
union
|
|
30956
|
+
};
|
|
30660
30957
|
//#endregion
|
|
30661
30958
|
//#region ../contracts/dist/runtime/schema.js
|
|
30662
30959
|
const FunctionSchemaTypeId = Symbol.for("ignotum/runtime/schema/FunctionSchema");
|
|
30960
|
+
const FunctionEnvironmentTypeId = Symbol.for("ignotum/runtime/schema/FunctionEnvironment");
|
|
30663
30961
|
const getFunctionSchema = (value) => value[FunctionSchemaTypeId];
|
|
30962
|
+
const getFunctionEnvironment = (value) => value[FunctionEnvironmentTypeId];
|
|
30664
30963
|
//#endregion
|
|
30665
30964
|
//#region ../contracts/dist/schema/index.js
|
|
30666
30965
|
const SchemaDefinitionTypeId = Symbol.for("ignotum/schema/SchemaDefinition");
|
|
30667
30966
|
const isDefinedSchema = (value) => value._tag === "Schema" && Predicate.isObject(value[SchemaDefinitionTypeId]);
|
|
30967
|
+
const EnvironmentDefinitionTypeId = Symbol.for("ignotum/schema/EnvironmentDefinition");
|
|
30968
|
+
const EnvironmentDecoderTypeId = Symbol.for("ignotum/schema/EnvironmentDecoder");
|
|
30969
|
+
const environmentLimits = {
|
|
30970
|
+
bytes: 65536,
|
|
30971
|
+
variables: 128
|
|
30972
|
+
};
|
|
30973
|
+
var EnvironmentValueInvalid = class extends Schema.TaggedError()("EnvironmentValueInvalid", {
|
|
30974
|
+
key: Schema.optional(Schema.String),
|
|
30975
|
+
message: Schema.String
|
|
30976
|
+
}) {};
|
|
30977
|
+
const environmentName = /^[A-Z_][A-Z0-9_]*$/;
|
|
30978
|
+
const defineEnv = (define) => {
|
|
30979
|
+
const fields = define({ values: environmentValues });
|
|
30980
|
+
if (Reflect.ownKeys(fields).length > environmentLimits.variables) throw new Error(`Environments cannot declare more than ${environmentLimits.variables} variables.`);
|
|
30981
|
+
for (const name of Reflect.ownKeys(fields)) {
|
|
30982
|
+
if (!Predicate.isString(name)) throw new Error("Environment variable names must be strings.");
|
|
30983
|
+
if (!environmentName.test(name)) throw new Error(`Environment variable '${name}' must match ${environmentName.source}.`);
|
|
30984
|
+
if (name.startsWith("IGNOTUM_")) throw new Error(`Environment variable '${name}' uses the reserved IGNOTUM_ prefix.`);
|
|
30985
|
+
}
|
|
30986
|
+
const descriptors = descriptorFields(fields);
|
|
30987
|
+
for (const field of descriptors) if (!descriptorAllowedInEnvironment(field.value)) throw new Error(`Environment variable '${field.name}' uses an unsupported validator.`);
|
|
30988
|
+
const environment = {
|
|
30989
|
+
_tag: "Environment",
|
|
30990
|
+
fields,
|
|
30991
|
+
descriptors,
|
|
30992
|
+
schema: Schema.Struct(fields),
|
|
30993
|
+
[EnvironmentDefinitionTypeId]: fields
|
|
30994
|
+
};
|
|
30995
|
+
Object.defineProperty(environment, EnvironmentDecoderTypeId, {
|
|
30996
|
+
enumerable: false,
|
|
30997
|
+
value: (raw) => decodeEnvironment(environment, raw)
|
|
30998
|
+
});
|
|
30999
|
+
return Object.freeze(environment);
|
|
31000
|
+
};
|
|
31001
|
+
const emptyEnv = defineEnv(() => ({}));
|
|
31002
|
+
const isDefinedEnv = (value) => value._tag === "Environment" && Predicate.isObject(value[EnvironmentDefinitionTypeId]);
|
|
31003
|
+
const utf8ByteLength = (value) => {
|
|
31004
|
+
let bytes = 0;
|
|
31005
|
+
for (const character of value) {
|
|
31006
|
+
const point = character.codePointAt(0) ?? 0;
|
|
31007
|
+
bytes += point <= 127 ? 1 : point <= 2047 ? 2 : point <= 65535 ? 3 : 4;
|
|
31008
|
+
}
|
|
31009
|
+
return bytes;
|
|
31010
|
+
};
|
|
31011
|
+
const invalidValue = (key) => EnvironmentValueInvalid.make({
|
|
31012
|
+
key,
|
|
31013
|
+
message: `Environment variable '${key}' does not match its declaration.`
|
|
31014
|
+
});
|
|
31015
|
+
const decodeEnvironment = Effect.fn("Environment.decode")(function* (environment, raw) {
|
|
31016
|
+
const declared = new Set(Object.keys(environment.fields));
|
|
31017
|
+
const supplied = Object.keys(raw);
|
|
31018
|
+
if (supplied.length > environmentLimits.variables) return yield* EnvironmentValueInvalid.make({ message: `Environments cannot contain more than ${environmentLimits.variables} variables.` });
|
|
31019
|
+
const rawJson = JSON.stringify(raw);
|
|
31020
|
+
if (utf8ByteLength(rawJson) > environmentLimits.bytes) return yield* EnvironmentValueInvalid.make({ message: `Environments cannot exceed ${environmentLimits.bytes} UTF-8 bytes.` });
|
|
31021
|
+
for (const key of supplied) if (!declared.has(key)) return yield* EnvironmentValueInvalid.make({
|
|
31022
|
+
key,
|
|
31023
|
+
message: `Environment variable '${key}' is not declared in server/env.ts.`
|
|
31024
|
+
});
|
|
31025
|
+
const decoded = {};
|
|
31026
|
+
for (const [key, field] of Object.entries(environment.fields)) {
|
|
31027
|
+
const input = Object.hasOwn(raw, key) ? raw[key] : void 0;
|
|
31028
|
+
const direct = yield* Effect.result(Schema.decodeUnknownEffect(field)(input));
|
|
31029
|
+
if (Result.isSuccess(direct)) {
|
|
31030
|
+
if (direct.success !== void 0) Reflect.set(decoded, key, direct.success);
|
|
31031
|
+
continue;
|
|
31032
|
+
}
|
|
31033
|
+
if (input === void 0) return yield* invalidValue(key);
|
|
31034
|
+
const parsed = yield* Effect.result(Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))(input));
|
|
31035
|
+
if (Result.isFailure(parsed)) return yield* invalidValue(key);
|
|
31036
|
+
const fromJson = yield* Effect.result(Schema.decodeUnknownEffect(field)(parsed.success));
|
|
31037
|
+
if (Result.isFailure(fromJson)) return yield* invalidValue(key);
|
|
31038
|
+
if (fromJson.success !== void 0) Reflect.set(decoded, key, fromJson.success);
|
|
31039
|
+
}
|
|
31040
|
+
return Object.freeze(decoded);
|
|
31041
|
+
});
|
|
31042
|
+
const decodeDefinedEnvironment = (environment, raw) => environment[EnvironmentDecoderTypeId](raw);
|
|
30668
31043
|
//#endregion
|
|
30669
31044
|
//#region ../deployment/dist/index.js
|
|
30670
31045
|
const encodeCanonical = Function$1.dual(2, (schema, value) => {
|
|
@@ -30729,6 +31104,7 @@ const artifactKind = (path) => {
|
|
|
30729
31104
|
if (path.startsWith(clientRoutePathPrefix) && isClientPublicFile(path)) return "ClientPublicFile";
|
|
30730
31105
|
if (path === serverManifestPath) return "ServerManifest";
|
|
30731
31106
|
if (path === schemaSnapshotPath) return "SchemaSnapshot";
|
|
31107
|
+
if (path === serverEnvironmentPath) return "ServerEnvironment";
|
|
30732
31108
|
if (path.startsWith("server/functions/") && path.endsWith(".mjs.map")) return "SourceMap";
|
|
30733
31109
|
if (path.startsWith("server/functions/") && path.endsWith(".mjs")) return "FunctionBundle";
|
|
30734
31110
|
};
|
|
@@ -30746,7 +31122,7 @@ const makeArtifactFile = Effect.fn("Deployment.makeArtifactFile")(function* (inp
|
|
|
30746
31122
|
const makeInventoryFromNormalized = Effect.fn("Deployment.makeInventoryFromNormalized")(function* (files) {
|
|
30747
31123
|
const inventoryFiles = yield* Effect.forEach(files, makeArtifactFile, { concurrency: "unbounded" });
|
|
30748
31124
|
return {
|
|
30749
|
-
formatVersion:
|
|
31125
|
+
formatVersion: 2,
|
|
30750
31126
|
files: Array$1.sortWith(inventoryFiles, (file) => file.path, String$1.Order)
|
|
30751
31127
|
};
|
|
30752
31128
|
});
|
|
@@ -30828,6 +31204,11 @@ const validateManifest = Effect.fn("Deployment.validateManifest")(function* (man
|
|
|
30828
31204
|
const addresses = /* @__PURE__ */ new Set();
|
|
30829
31205
|
const referencedPaths = /* @__PURE__ */ new Set([serverManifestPath, schemaSnapshotPath]);
|
|
30830
31206
|
const references = [manifest.schema];
|
|
31207
|
+
if (manifest.environment !== void 0) {
|
|
31208
|
+
if (manifest.environment.artifact.path !== serverEnvironmentPath) return yield* invalid$3(`The server manifest must reference '${serverEnvironmentPath}' as its environment.`, manifest.environment.artifact.path);
|
|
31209
|
+
references.push(manifest.environment.artifact);
|
|
31210
|
+
referencedPaths.add(serverEnvironmentPath);
|
|
31211
|
+
}
|
|
30831
31212
|
for (const definition of manifest.functions) {
|
|
30832
31213
|
if (addresses.has(definition.address)) return yield* invalid$3(`Duplicate function address '${definition.address}'.`);
|
|
30833
31214
|
addresses.add(definition.address);
|
|
@@ -30845,6 +31226,26 @@ const validateManifest = Effect.fn("Deployment.validateManifest")(function* (man
|
|
|
30845
31226
|
}
|
|
30846
31227
|
for (const file of inventory.files) if (file.path.startsWith("server/") && !referencedPaths.has(file.path)) return yield* invalid$3(`Unexpected server entrypoint '${file.path}'.`, file.path);
|
|
30847
31228
|
});
|
|
31229
|
+
const validateArtifactFile = Effect.fn("Deployment.validateArtifactFile")(function* (file, entry) {
|
|
31230
|
+
const actual = yield* makeArtifactFile(file);
|
|
31231
|
+
if (entry === void 0 || encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${file.path}' does not match the deployment inventory.`, file.path);
|
|
31232
|
+
});
|
|
31233
|
+
const decodeServerMetadata = Effect.fn("Deployment.decodeServerMetadata")(function* (files, inventory) {
|
|
31234
|
+
const manifest = yield* decodeJsonFile(ServerBuildManifest, yield* findFile(files, serverManifestPath));
|
|
31235
|
+
const schema = yield* decodeJsonFile(SchemaSnapshot, yield* findFile(files, schemaSnapshotPath));
|
|
31236
|
+
yield* validateManifest(manifest, inventory);
|
|
31237
|
+
let environment;
|
|
31238
|
+
if (manifest.environment !== void 0) {
|
|
31239
|
+
environment = yield* decodeJsonFile(EnvironmentArtifact, yield* findFile(files, serverEnvironmentPath));
|
|
31240
|
+
if (Object.keys(environment.values).length > environmentLimits.variables) return yield* invalid$3("The server environment contains too many variables.", serverEnvironmentPath);
|
|
31241
|
+
if (utf8Bytes(encodeCanonicalJson(environment.values)).byteLength > environmentLimits.bytes) return yield* invalid$3("The server environment exceeds its size limit.", serverEnvironmentPath);
|
|
31242
|
+
}
|
|
31243
|
+
return {
|
|
31244
|
+
manifest,
|
|
31245
|
+
schema,
|
|
31246
|
+
environment
|
|
31247
|
+
};
|
|
31248
|
+
});
|
|
30848
31249
|
Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
30849
31250
|
yield* validateDeploymentInventory(input.inventory);
|
|
30850
31251
|
const inventoryFile = {
|
|
@@ -30871,22 +31272,21 @@ Effect.fn("Deployment.validateDeploymentMetadata")(function* (input) {
|
|
|
30871
31272
|
manifestFile,
|
|
30872
31273
|
schemaFile
|
|
30873
31274
|
];
|
|
31275
|
+
if (input.environmentBytes !== void 0) metadataFiles.push({
|
|
31276
|
+
path: serverEnvironmentPath,
|
|
31277
|
+
bytes: input.environmentBytes
|
|
31278
|
+
});
|
|
30874
31279
|
const entries = new Map(input.inventory.files.map((file) => [file.path, file]));
|
|
30875
|
-
for (const file of metadataFiles)
|
|
30876
|
-
const entry = entries.get(file.path);
|
|
30877
|
-
const actual = yield* makeArtifactFile(file);
|
|
30878
|
-
if (entry === void 0 || encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${file.path}' does not match the deployment inventory.`, file.path);
|
|
30879
|
-
}
|
|
31280
|
+
for (const file of metadataFiles) yield* validateArtifactFile(file, entries.get(file.path));
|
|
30880
31281
|
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30881
|
-
const manifest = yield*
|
|
30882
|
-
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
31282
|
+
const { manifest, schema, environment } = yield* decodeServerMetadata(metadataFiles, input.inventory);
|
|
30883
31283
|
yield* validateClientManifest(clientManifest, input.inventory);
|
|
30884
|
-
yield* validateManifest(manifest, input.inventory);
|
|
30885
31284
|
return {
|
|
30886
31285
|
clientManifest,
|
|
30887
31286
|
inventory: input.inventory,
|
|
30888
31287
|
manifest,
|
|
30889
|
-
schema
|
|
31288
|
+
schema,
|
|
31289
|
+
environment
|
|
30890
31290
|
};
|
|
30891
31291
|
});
|
|
30892
31292
|
const validateDeploymentArtifact = Effect.fn("Deployment.validateDeploymentArtifact")(function* (inputs) {
|
|
@@ -30898,21 +31298,18 @@ const validateDeploymentArtifact = Effect.fn("Deployment.validateDeploymentArtif
|
|
|
30898
31298
|
const actualInventory = yield* makeInventoryFromNormalized(payloadFiles);
|
|
30899
31299
|
if (encodeCanonical(DeploymentInventory, inventory) !== encodeCanonical(DeploymentInventory, actualInventory)) return yield* invalid$3("The deployment inventory does not match the artifact files.");
|
|
30900
31300
|
const clientManifestFile = yield* findFile(files, clientManifestPath);
|
|
30901
|
-
const manifestFile = yield* findFile(files, serverManifestPath);
|
|
30902
|
-
const schemaFile = yield* findFile(files, schemaSnapshotPath);
|
|
30903
31301
|
const clientManifest = yield* decodeJsonFile(ClientManifest, clientManifestFile);
|
|
30904
|
-
const manifest = yield*
|
|
30905
|
-
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
31302
|
+
const { manifest, schema, environment } = yield* decodeServerMetadata(files, inventory);
|
|
30906
31303
|
yield* validateClientManifest(clientManifest, inventory);
|
|
30907
|
-
yield* validateManifest(manifest, inventory);
|
|
30908
31304
|
return {
|
|
30909
31305
|
clientManifest,
|
|
31306
|
+
environment,
|
|
30910
31307
|
inventory,
|
|
30911
31308
|
manifest,
|
|
30912
31309
|
schema
|
|
30913
31310
|
};
|
|
30914
31311
|
});
|
|
30915
|
-
Effect.fn("Deployment.loadRuntimeDeploymentArtifact")(function* (read) {
|
|
31312
|
+
Effect.fn("Deployment.loadRuntimeDeploymentArtifact")(function* (read, readEnvironment = read) {
|
|
30916
31313
|
const inventoryBytes = yield* read(deploymentInventoryPath, deploymentArtifactLimits.inventoryBytes);
|
|
30917
31314
|
const inventory = yield* decodeJsonFile(DeploymentInventory, {
|
|
30918
31315
|
path: deploymentInventoryPath,
|
|
@@ -30921,23 +31318,20 @@ Effect.fn("Deployment.loadRuntimeDeploymentArtifact")(function* (read) {
|
|
|
30921
31318
|
yield* validateDeploymentInventory(inventory);
|
|
30922
31319
|
const serverEntries = inventory.files.filter((file) => file.path.startsWith("server/"));
|
|
30923
31320
|
const serverFiles = yield* Effect.forEach(serverEntries, (entry) => Effect.gen(function* () {
|
|
31321
|
+
const reader = entry.kind === "ServerEnvironment" ? readEnvironment : read;
|
|
30924
31322
|
const input = {
|
|
30925
31323
|
path: entry.path,
|
|
30926
|
-
bytes: yield*
|
|
31324
|
+
bytes: yield* reader(entry.path, entry.size)
|
|
30927
31325
|
};
|
|
30928
|
-
|
|
30929
|
-
if (encodeCanonical(ArtifactFile, actual) !== encodeCanonical(ArtifactFile, entry)) return yield* invalid$3(`Artifact file '${entry.path}' does not match the deployment inventory.`, entry.path);
|
|
31326
|
+
yield* validateArtifactFile(input, entry);
|
|
30930
31327
|
return input;
|
|
30931
31328
|
}), { concurrency: 4 });
|
|
30932
|
-
const
|
|
30933
|
-
const schemaFile = yield* findFile(serverFiles, schemaSnapshotPath);
|
|
30934
|
-
const manifest = yield* decodeJsonFile(ServerBuildManifest, manifestFile);
|
|
30935
|
-
const schema = yield* decodeJsonFile(SchemaSnapshot, schemaFile);
|
|
30936
|
-
yield* validateManifest(manifest, inventory);
|
|
31329
|
+
const { manifest, schema, environment } = yield* decodeServerMetadata(serverFiles, inventory);
|
|
30937
31330
|
return {
|
|
30938
31331
|
inventory,
|
|
30939
31332
|
manifest,
|
|
30940
31333
|
schema,
|
|
31334
|
+
environment,
|
|
30941
31335
|
files: new Map(serverFiles.map((file) => [file.path, file.bytes]))
|
|
30942
31336
|
};
|
|
30943
31337
|
});
|
|
@@ -31224,19 +31618,19 @@ const resolvedClientEntryId = `\0${clientEntryId}`;
|
|
|
31224
31618
|
const clientStylesId = "virtual:ignotum/_styles.css";
|
|
31225
31619
|
const resolvedClientStylesId = `\0${clientStylesId}`;
|
|
31226
31620
|
const clientStylesSource = "@import \"tailwindcss\";\n";
|
|
31227
|
-
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
31621
|
+
const escapeHtml$1 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
31228
31622
|
const renderClientDocument = (options) => `<!doctype html>
|
|
31229
31623
|
<html lang="en">
|
|
31230
31624
|
<head>
|
|
31231
31625
|
<meta charset="UTF-8" />
|
|
31232
31626
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
31233
|
-
<title>${escapeHtml(options.title)}</title>
|
|
31234
|
-
${options.icon === void 0 ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml(options.icon)}" />`}
|
|
31235
|
-
${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")}
|
|
31236
31630
|
</head>
|
|
31237
31631
|
<body>
|
|
31238
31632
|
<div id="app"></div>
|
|
31239
|
-
${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")}
|
|
31240
31634
|
</body>
|
|
31241
31635
|
</html>`;
|
|
31242
31636
|
const renderClientEntry = (development) => `${development ? "import \"preact/debug\"\n" : ""}import { render } from "preact"
|
|
@@ -31658,6 +32052,78 @@ const inspectRuntimeFunctionDefinition = (value) => {
|
|
|
31658
32052
|
};
|
|
31659
32053
|
};
|
|
31660
32054
|
//#endregion
|
|
32055
|
+
//#region src/cli/environment.ts
|
|
32056
|
+
var EnvironmentConfigurationInvalid = class extends Schema.TaggedError()("EnvironmentConfigurationInvalid", {
|
|
32057
|
+
message: Schema.String,
|
|
32058
|
+
path: Schema.String
|
|
32059
|
+
}) {};
|
|
32060
|
+
const collectKeys = Effect.fn("Environment.collectKeys")(function* (provider, providerPath = [], nameParts = []) {
|
|
32061
|
+
const node = yield* provider.load(providerPath);
|
|
32062
|
+
if (node === void 0) return [];
|
|
32063
|
+
const own = node._tag === "Value" || node.value !== void 0 ? [nameParts.join("_")] : [];
|
|
32064
|
+
if (node._tag === "Value") return own;
|
|
32065
|
+
if (node._tag === "Array") {
|
|
32066
|
+
const children = yield* Effect.forEach(Array$1.range(0, node.length - 1), (index) => collectKeys(provider, [...providerPath, index], [...nameParts, index.toString()]));
|
|
32067
|
+
return [...own, ...Array$1.flatten(children)];
|
|
32068
|
+
}
|
|
32069
|
+
const children = yield* Effect.forEach(Array$1.fromIterable(node.keys), (key) => collectKeys(provider, [...providerPath, key], [...nameParts, key]));
|
|
32070
|
+
return [...own, ...Array$1.flatten(children)];
|
|
32071
|
+
});
|
|
32072
|
+
const nodeValue = (node) => node?._tag === "Value" ? node.value : node?.value;
|
|
32073
|
+
const readEnvironmentRaw = Effect.fn("Environment.readRaw")(function* (appDirectory, hasDefinition) {
|
|
32074
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
32075
|
+
const environmentPath = (yield* Path$1.Path).join(appDirectory, ".env.ignotum");
|
|
32076
|
+
const exists = yield* fileSystem.exists(environmentPath);
|
|
32077
|
+
if (!hasDefinition && exists) return yield* EnvironmentConfigurationInvalid.make({
|
|
32078
|
+
message: ".env.ignotum exists but server/env.ts does not define its variables.",
|
|
32079
|
+
path: environmentPath
|
|
32080
|
+
});
|
|
32081
|
+
if (!hasDefinition) return {
|
|
32082
|
+
path: environmentPath,
|
|
32083
|
+
raw: {}
|
|
32084
|
+
};
|
|
32085
|
+
if (!exists) return yield* EnvironmentConfigurationInvalid.make({
|
|
32086
|
+
message: "server/env.ts requires a project-root .env.ignotum file.",
|
|
32087
|
+
path: environmentPath
|
|
32088
|
+
});
|
|
32089
|
+
const provider = yield* ConfigProvider.fromDotEnv({
|
|
32090
|
+
path: environmentPath,
|
|
32091
|
+
preserveEmptyStrings: true
|
|
32092
|
+
}).pipe(Effect.mapError(() => EnvironmentConfigurationInvalid.make({
|
|
32093
|
+
message: "Ignotum could not read the project environment file.",
|
|
32094
|
+
path: environmentPath
|
|
32095
|
+
})));
|
|
32096
|
+
const keys = yield* collectKeys(provider).pipe(Effect.mapError(() => EnvironmentConfigurationInvalid.make({
|
|
32097
|
+
message: "Ignotum could not inspect the project environment file.",
|
|
32098
|
+
path: environmentPath
|
|
32099
|
+
})));
|
|
32100
|
+
const entries = yield* Effect.forEach(keys, (key) => provider.load([key]).pipe(Effect.map((node) => [key, nodeValue(node)]), Effect.mapError(() => EnvironmentConfigurationInvalid.make({
|
|
32101
|
+
message: `Ignotum could not read environment variable '${key}'.`,
|
|
32102
|
+
path: environmentPath
|
|
32103
|
+
}))));
|
|
32104
|
+
return {
|
|
32105
|
+
path: environmentPath,
|
|
32106
|
+
raw: Object.fromEntries(entries.flatMap(([key, value]) => value === void 0 ? [] : [[key, value]]))
|
|
32107
|
+
};
|
|
32108
|
+
});
|
|
32109
|
+
const loadEnvironment = Effect.fn("Environment.load")(function* (appDirectory, definition, hasDefinition) {
|
|
32110
|
+
const loaded = yield* readEnvironmentRaw(appDirectory, hasDefinition);
|
|
32111
|
+
if (!hasDefinition) return {
|
|
32112
|
+
definition: emptyEnv,
|
|
32113
|
+
raw: {},
|
|
32114
|
+
value: Object.freeze({})
|
|
32115
|
+
};
|
|
32116
|
+
const raw = loaded.raw;
|
|
32117
|
+
return {
|
|
32118
|
+
definition,
|
|
32119
|
+
raw,
|
|
32120
|
+
value: yield* decodeDefinedEnvironment(definition, raw).pipe(Effect.mapError((error) => EnvironmentConfigurationInvalid.make({
|
|
32121
|
+
message: error.message,
|
|
32122
|
+
path: loaded.path
|
|
32123
|
+
})))
|
|
32124
|
+
};
|
|
32125
|
+
});
|
|
32126
|
+
//#endregion
|
|
31661
32127
|
//#region src/cli/build/server.ts
|
|
31662
32128
|
const hostModuleId = "ignotum:host";
|
|
31663
32129
|
const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.String));
|
|
@@ -31672,6 +32138,11 @@ const DiscoveredServerFunction = Schema.Struct({
|
|
|
31672
32138
|
returns: Schema.optional(ValueDescriptor)
|
|
31673
32139
|
});
|
|
31674
32140
|
const ServerDiscoveryManifest = Schema.Struct({
|
|
32141
|
+
environment: Schema.Struct({
|
|
32142
|
+
fields: Schema.Array(SchemaSnapshotField),
|
|
32143
|
+
hasDefinition: Schema.Boolean,
|
|
32144
|
+
values: Schema.Record(Schema.String, Schema.String)
|
|
32145
|
+
}),
|
|
31675
32146
|
functions: Schema.Array(DiscoveredServerFunction),
|
|
31676
32147
|
schema: SchemaSnapshot
|
|
31677
32148
|
});
|
|
@@ -31804,7 +32275,9 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
|
|
|
31804
32275
|
const discoverServerFunctionsInProcess = Effect.fn("Deploy.discoverServerFunctions")(function* (appDirectory, moduleNames, conditions) {
|
|
31805
32276
|
return yield* Effect.scoped(Effect.gen(function* () {
|
|
31806
32277
|
const server = yield* acquireModuleRunner(appDirectory, conditions);
|
|
31807
|
-
const
|
|
32278
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
32279
|
+
const path = yield* Path$1.Path;
|
|
32280
|
+
const schemaPath = path.join(appDirectory, "server", "schema.ts");
|
|
31808
32281
|
const loadedSchemaModule = yield* Effect.tryPromise({
|
|
31809
32282
|
try: () => server.ssrLoadModule(normalizePath(schemaPath)),
|
|
31810
32283
|
catch: (cause) => Schema.is(BuildBoundaryViolation)(cause) ? cause : ServerFunctionDiscoveryFailed.make({
|
|
@@ -31815,15 +32288,37 @@ const discoverServerFunctionsInProcess = Effect.fn("Deploy.discoverServerFunctio
|
|
|
31815
32288
|
});
|
|
31816
32289
|
const schemaCandidate = yield* Schema.decodeUnknownEffect(Schema.ObjectKeyword)(loadedSchemaModule.default).pipe(Effect.mapError(() => InvalidBuildOutput.make({ message: "The default export from server/schema.ts is not an Ignotum schema." })));
|
|
31817
32290
|
if (!isDefinedSchema(schemaCandidate)) return yield* InvalidBuildOutput.make({ message: "The default export from server/schema.ts is not an Ignotum schema." });
|
|
32291
|
+
const environmentPath = path.join(appDirectory, "server", "env.ts");
|
|
32292
|
+
const environment = (yield* fileSystem.exists(environmentPath)) ? yield* Effect.gen(function* () {
|
|
32293
|
+
const loadedEnvironmentModule = yield* Effect.tryPromise({
|
|
32294
|
+
try: () => server.ssrLoadModule(normalizePath(environmentPath)),
|
|
32295
|
+
catch: (cause) => ServerFunctionDiscoveryFailed.make({
|
|
32296
|
+
cause,
|
|
32297
|
+
message: "Could not load the application environment declaration.",
|
|
32298
|
+
path: environmentPath
|
|
32299
|
+
})
|
|
32300
|
+
});
|
|
32301
|
+
if (!Predicate.isObject(loadedEnvironmentModule.default) || !isDefinedEnv(loadedEnvironmentModule.default)) return yield* InvalidBuildOutput.make({ message: "The default export from server/env.ts is not an Ignotum environment." });
|
|
32302
|
+
const loaded = yield* loadEnvironment(appDirectory, loadedEnvironmentModule.default, true);
|
|
32303
|
+
return {
|
|
32304
|
+
fields: loadedEnvironmentModule.default.descriptors,
|
|
32305
|
+
hasDefinition: true,
|
|
32306
|
+
values: loaded.raw
|
|
32307
|
+
};
|
|
32308
|
+
}) : yield* loadEnvironment(appDirectory, emptyEnv, false).pipe(Effect.map((loaded) => ({
|
|
32309
|
+
fields: [],
|
|
32310
|
+
hasDefinition: false,
|
|
32311
|
+
values: loaded.raw
|
|
32312
|
+
})));
|
|
31818
32313
|
const modules = yield* Effect.forEach(moduleNames, (moduleName) => discoverModuleFunctions(server, appDirectory, moduleName));
|
|
31819
32314
|
return {
|
|
31820
32315
|
functions: Array$1.flatten(modules),
|
|
32316
|
+
environment,
|
|
31821
32317
|
schema: schemaCandidate
|
|
31822
32318
|
};
|
|
31823
32319
|
}));
|
|
31824
32320
|
});
|
|
31825
|
-
const runServerDiscoveryWorker = Effect.fn("Deploy.runServerDiscoveryWorker")(function* (appDirectory, moduleNamesJson
|
|
31826
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
32321
|
+
const runServerDiscoveryWorker = Effect.fn("Deploy.runServerDiscoveryWorker")(function* (appDirectory, moduleNamesJson) {
|
|
31827
32322
|
const moduleNames = yield* Schema.decodeEffect(ServerModuleNamesJson)(moduleNamesJson).pipe(Effect.orDie);
|
|
31828
32323
|
const conditions = yield* serverConditions();
|
|
31829
32324
|
const discovered = yield* discoverServerFunctionsInProcess(appDirectory, moduleNames, conditions);
|
|
@@ -31831,11 +32326,11 @@ const runServerDiscoveryWorker = Effect.fn("Deploy.runServerDiscoveryWorker")(fu
|
|
|
31831
32326
|
try: () => makeSchemaSnapshot(discovered.schema),
|
|
31832
32327
|
catch: () => InvalidBuildOutput.make({ message: "The application schema does not have canonical Ignotum descriptors." })
|
|
31833
32328
|
});
|
|
31834
|
-
|
|
32329
|
+
return yield* Schema.encodeEffect(ServerDiscoveryManifestJson)({
|
|
32330
|
+
environment: discovered.environment,
|
|
31835
32331
|
functions: discovered.functions,
|
|
31836
32332
|
schema
|
|
31837
32333
|
});
|
|
31838
|
-
yield* fileSystem.writeFileString(outputPath, encoded);
|
|
31839
32334
|
});
|
|
31840
32335
|
const discoveryWorkerEntry = Effect.fn("Deploy.discoveryWorkerEntry")(function* () {
|
|
31841
32336
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
@@ -31859,7 +32354,6 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
31859
32354
|
force: true,
|
|
31860
32355
|
recursive: true
|
|
31861
32356
|
}).pipe(Effect.orDie));
|
|
31862
|
-
const outputPath = path.join(directory, "discovery.json");
|
|
31863
32357
|
const encodedModuleNames = yield* Schema.encodeEffect(ServerModuleNamesJson)(moduleNames);
|
|
31864
32358
|
const packageRoot = path.resolve(path.dirname(workerEntry), "../..");
|
|
31865
32359
|
const workspaceProbes = [];
|
|
@@ -31879,20 +32373,22 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
31879
32373
|
const readPermissions = [
|
|
31880
32374
|
appDirectory,
|
|
31881
32375
|
nodeModulesDirectory(),
|
|
32376
|
+
path.dirname(nodeModulesDirectory()),
|
|
31882
32377
|
packageRoot,
|
|
31883
32378
|
path.join(packageRoot, "node_modules"),
|
|
32379
|
+
path.resolve(packageRoot, "../..", "node_modules"),
|
|
31884
32380
|
...workspaceProbes
|
|
31885
32381
|
].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
|
|
31886
|
-
const
|
|
32382
|
+
const handle = yield* spawner.spawn(ChildProcess.make(process$1.execPath, [
|
|
31887
32383
|
"--permission",
|
|
31888
32384
|
"--allow-addons",
|
|
32385
|
+
"--conditions=@ignotum/source",
|
|
31889
32386
|
...readPermissions,
|
|
31890
32387
|
`--allow-fs-write=${directory}`,
|
|
31891
32388
|
workerEntry,
|
|
31892
32389
|
"__ignotum_discover_server",
|
|
31893
32390
|
appDirectory,
|
|
31894
|
-
encodedModuleNames
|
|
31895
|
-
outputPath
|
|
32391
|
+
encodedModuleNames
|
|
31896
32392
|
], {
|
|
31897
32393
|
cwd: appDirectory,
|
|
31898
32394
|
env: {
|
|
@@ -31901,18 +32397,19 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
31901
32397
|
NODE_ENV: "production"
|
|
31902
32398
|
},
|
|
31903
32399
|
stderr: "inherit",
|
|
31904
|
-
stdout: "ignore"
|
|
32400
|
+
stdout: "ignore",
|
|
32401
|
+
additionalFds: { fd3: { type: "output" } }
|
|
31905
32402
|
}));
|
|
32403
|
+
const [encoded, exitCode] = yield* Effect.all([Stream.mkString(Stream.decodeText(handle.getOutputFd(3))), handle.exitCode], { concurrency: "unbounded" });
|
|
31906
32404
|
if (exitCode !== ChildProcessSpawner$1.ExitCode(0)) return yield* ServerFunctionDiscoveryFailed.make({
|
|
31907
32405
|
cause: /* @__PURE__ */ new Error(`Discovery worker exited with code ${exitCode}.`),
|
|
31908
32406
|
message: "Application metadata discovery failed in the isolated build process.",
|
|
31909
32407
|
path: appDirectory
|
|
31910
32408
|
});
|
|
31911
|
-
const encoded = yield* fileSystem.readFileString(outputPath);
|
|
31912
32409
|
return yield* Schema.decodeEffect(ServerDiscoveryManifestJson)(encoded).pipe(Effect.mapError((cause) => ServerFunctionDiscoveryFailed.make({
|
|
31913
32410
|
cause,
|
|
31914
32411
|
message: "The isolated build process returned invalid application metadata.",
|
|
31915
|
-
path:
|
|
32412
|
+
path: appDirectory
|
|
31916
32413
|
})));
|
|
31917
32414
|
}));
|
|
31918
32415
|
});
|
|
@@ -31923,8 +32420,8 @@ const serverFunctionEntryPlugin = (definition) => {
|
|
|
31923
32420
|
import { invoke } from "ignotum/internal/host"
|
|
31924
32421
|
import { ${definition.exportName} as definition } from ${encodeJavaScriptString(normalizePath(definition.modulePath))}
|
|
31925
32422
|
|
|
31926
|
-
export default async function handler(encodedRequest) {
|
|
31927
|
-
return invoke(definition, JSON.parse(encodedRequest), call)
|
|
32423
|
+
export default async function handler(encodedRequest, encodedEnvironment) {
|
|
32424
|
+
return invoke(definition, JSON.parse(encodedRequest), call, JSON.parse(encodedEnvironment))
|
|
31928
32425
|
}
|
|
31929
32426
|
`;
|
|
31930
32427
|
return {
|
|
@@ -32011,13 +32508,24 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
|
|
|
32011
32508
|
const path = yield* Path$1.Path;
|
|
32012
32509
|
const conditions = yield* serverConditions();
|
|
32013
32510
|
const discovered = yield* discoverServerFunctions(appDirectory, functionModules);
|
|
32511
|
+
const crypto = yield* Crypto.Crypto;
|
|
32512
|
+
const environmentArtifact = {
|
|
32513
|
+
formatVersion: 1,
|
|
32514
|
+
nonce: Encoding.encodeBase64Url(yield* crypto.randomBytes(32)),
|
|
32515
|
+
values: discovered.environment.values
|
|
32516
|
+
};
|
|
32517
|
+
const environmentBytes = utf8Bytes(`${encodeCanonical(EnvironmentArtifact, environmentArtifact)}\n`);
|
|
32014
32518
|
yield* fileSystem.makeDirectory(outputDirectory, { recursive: true });
|
|
32015
32519
|
const encodedSnapshot = encodeSchemaSnapshot(discovered.schema);
|
|
32016
32520
|
const snapshotBytes = utf8Bytes(encodedSnapshot);
|
|
32017
32521
|
yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
|
|
32018
32522
|
const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
|
|
32019
32523
|
const manifest = {
|
|
32020
|
-
formatVersion:
|
|
32524
|
+
formatVersion: 3,
|
|
32525
|
+
environment: {
|
|
32526
|
+
artifact: yield* artifactReference(serverEnvironmentPath, environmentBytes),
|
|
32527
|
+
fields: discovered.environment.fields
|
|
32528
|
+
},
|
|
32021
32529
|
schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
|
|
32022
32530
|
functions
|
|
32023
32531
|
};
|
|
@@ -32026,6 +32534,10 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
|
|
|
32026
32534
|
yield* fileSystem.writeFileString(manifestPath, encodedManifest);
|
|
32027
32535
|
yield* Schema.decodeEffect(Schema.fromJsonString(ServerBuildManifest))(encodedManifest).pipe(Effect.mapError(() => InvalidBuildOutput.make({ message: "Ignotum emitted an invalid server manifest." })));
|
|
32028
32536
|
return {
|
|
32537
|
+
environment: {
|
|
32538
|
+
bytes: environmentBytes,
|
|
32539
|
+
path: serverEnvironmentPath
|
|
32540
|
+
},
|
|
32029
32541
|
functions: functions.length,
|
|
32030
32542
|
manifestPath
|
|
32031
32543
|
};
|
|
@@ -32091,10 +32603,10 @@ const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (app
|
|
|
32091
32603
|
const serverOutput = path.join(stagingDirectory, "server");
|
|
32092
32604
|
const client = yield* buildClient(appDirectory, clientOutput);
|
|
32093
32605
|
const server = yield* buildServer(appDirectory, serverOutput, codegen.functionModules);
|
|
32094
|
-
const payloadFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
32606
|
+
const payloadFiles = [...yield* readArtifactDirectory(stagingDirectory), server.environment];
|
|
32095
32607
|
const inventory = yield* makeDeploymentInventory(payloadFiles);
|
|
32096
32608
|
yield* fileSystem.writeFile(path.join(stagingDirectory, deploymentInventoryPath), inventory.bytes);
|
|
32097
|
-
const artifactFiles = yield* readArtifactDirectory(stagingDirectory);
|
|
32609
|
+
const artifactFiles = [...yield* readArtifactDirectory(stagingDirectory), server.environment];
|
|
32098
32610
|
yield* validateDeploymentArtifact(artifactFiles);
|
|
32099
32611
|
yield* Effect.logInfo("Built deployment artifact").pipe(Effect.annotateLogs({
|
|
32100
32612
|
files: inventory.inventory.files.length,
|
|
@@ -32105,6 +32617,7 @@ const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (app
|
|
|
32105
32617
|
clientAssets: client.assets,
|
|
32106
32618
|
inventory: inventory.inventory,
|
|
32107
32619
|
inventoryBytes: inventory.bytes,
|
|
32620
|
+
memoryFiles: /* @__PURE__ */ new Map([[server.environment.path, server.environment.bytes]]),
|
|
32108
32621
|
outputDirectory: buildDirectory,
|
|
32109
32622
|
serverFunctions: server.functions
|
|
32110
32623
|
};
|
|
@@ -32113,7 +32626,7 @@ const buildDeploymentArtifact = Effect.fn("Deploy.buildArtifact")(function* (app
|
|
|
32113
32626
|
const uploadDeployment = Effect.fn("Deploy.upload")(function* (configuration, build) {
|
|
32114
32627
|
const control = yield* ControlClient;
|
|
32115
32628
|
const inventorySha256 = yield* sha256(build.inventoryBytes);
|
|
32116
|
-
const files = yield* deploymentUploadFiles(build.inventory, inventorySha256, build.inventoryBytes.byteLength, build.outputDirectory);
|
|
32629
|
+
const files = yield* deploymentUploadFiles(build.inventory, inventorySha256, build.inventoryBytes.byteLength, build.outputDirectory, build.memoryFiles);
|
|
32117
32630
|
const deployment = yield* control.beginDeployment(configuration.appId, build.inventory);
|
|
32118
32631
|
yield* Effect.forEach(files, (file) => control.uploadDeploymentFile(configuration.appId, deployment.id, file), {
|
|
32119
32632
|
concurrency: 4,
|
|
@@ -32277,195 +32790,6 @@ const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (appDirectory)
|
|
|
32277
32790
|
}));
|
|
32278
32791
|
});
|
|
32279
32792
|
//#endregion
|
|
32280
|
-
//#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
|
|
32281
|
-
/**
|
|
32282
|
-
* Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
|
|
32283
|
-
*
|
|
32284
|
-
* This module opens a SQLite database and exposes it as both `SqliteClient` and
|
|
32285
|
-
* the generic Effect SQL client. It serializes access through one connection,
|
|
32286
|
-
* caches prepared statements, enables WAL mode unless disabled, and waits up
|
|
32287
|
-
* to five seconds for busy databases by default. Explicit transactions on
|
|
32288
|
-
* writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock
|
|
32289
|
-
* upgrades, which serializes them behind other writers even when they only
|
|
32290
|
-
* read. Clients opened with `readonly: true` are unaffected. Busy waits block
|
|
32291
|
-
* the Node.js event loop because `node:sqlite` is synchronous. Database backup
|
|
32292
|
-
* and extension loading are supported; streaming queries and `updateValues`
|
|
32293
|
-
* are not.
|
|
32294
|
-
*
|
|
32295
|
-
* @since 4.0.0
|
|
32296
|
-
*/
|
|
32297
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
32298
|
-
const MAX_BUSY_TIMEOUT = 2147483647;
|
|
32299
|
-
/**
|
|
32300
|
-
* Runtime type identifier used to mark Node `SqliteClient` values.
|
|
32301
|
-
*
|
|
32302
|
-
* @category type IDs
|
|
32303
|
-
* @since 4.0.0
|
|
32304
|
-
*/
|
|
32305
|
-
const TypeId$1 = "~@effect/sql-sqlite-node/SqliteClient";
|
|
32306
|
-
/**
|
|
32307
|
-
* Service tag for the node SQLite client implementation.
|
|
32308
|
-
*
|
|
32309
|
-
* @category services
|
|
32310
|
-
* @since 4.0.0
|
|
32311
|
-
*/
|
|
32312
|
-
const SqliteClient = /*#__PURE__*/ Context$1.Service("@effect/sql-sqlite-node/SqliteClient");
|
|
32313
|
-
/**
|
|
32314
|
-
* 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.
|
|
32315
|
-
*
|
|
32316
|
-
* @category constructors
|
|
32317
|
-
* @since 4.0.0
|
|
32318
|
-
*/
|
|
32319
|
-
const make$2 = (options) => Effect$1.gen(function* () {
|
|
32320
|
-
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
|
|
32321
|
-
const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : void 0;
|
|
32322
|
-
const makeConnection = Effect$1.gen(function* () {
|
|
32323
|
-
const scope = yield* Effect$1.scope;
|
|
32324
|
-
const db = new DatabaseSync(options.filename, {
|
|
32325
|
-
readOnly: options.readonly ?? false,
|
|
32326
|
-
allowExtension: true
|
|
32327
|
-
});
|
|
32328
|
-
yield* Scope.addFinalizer(scope, Effect$1.sync(() => db.close()));
|
|
32329
|
-
db.enableLoadExtension(false);
|
|
32330
|
-
const busyTimeout = Math.min(MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))));
|
|
32331
|
-
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
32332
|
-
if (options.disableWAL !== true) db.exec("PRAGMA journal_mode = WAL");
|
|
32333
|
-
const prepareCache = yield* Cache.make({
|
|
32334
|
-
capacity: options.prepareCacheSize ?? 200,
|
|
32335
|
-
timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
|
|
32336
|
-
lookup: (sql) => Effect$1.try({
|
|
32337
|
-
try: () => db.prepare(sql),
|
|
32338
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
|
|
32339
|
-
})
|
|
32340
|
-
});
|
|
32341
|
-
const runStatement = (statement, params, raw) => Effect$1.withFiber((fiber) => {
|
|
32342
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32343
|
-
return Effect$1.try({
|
|
32344
|
-
try: () => {
|
|
32345
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32346
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32347
|
-
const result = statement.run(...params);
|
|
32348
|
-
return raw ? {
|
|
32349
|
-
changes: result.changes,
|
|
32350
|
-
lastInsertRowid: result.lastInsertRowid
|
|
32351
|
-
} : [];
|
|
32352
|
-
},
|
|
32353
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32354
|
-
});
|
|
32355
|
-
});
|
|
32356
|
-
const runStatementValues = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
32357
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32358
|
-
return Effect$1.try({
|
|
32359
|
-
try: () => {
|
|
32360
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32361
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32362
|
-
statement.run(...params);
|
|
32363
|
-
return [];
|
|
32364
|
-
},
|
|
32365
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32366
|
-
});
|
|
32367
|
-
});
|
|
32368
|
-
const runStatementValuesUnprepared = (statement, params) => Effect$1.withFiber((fiber) => {
|
|
32369
|
-
const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
|
|
32370
|
-
return Effect$1.try({
|
|
32371
|
-
try: () => {
|
|
32372
|
-
statement.setReadBigInts(useSafeIntegers);
|
|
32373
|
-
statement.setReturnArrays(true);
|
|
32374
|
-
if (statement.columns().length > 0) return statement.all(...params);
|
|
32375
|
-
statement.run(...params);
|
|
32376
|
-
return [];
|
|
32377
|
-
},
|
|
32378
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
|
|
32379
|
-
});
|
|
32380
|
-
});
|
|
32381
|
-
const run = (sql, params, raw = false) => Effect$1.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
|
|
32382
|
-
const runValues = (sql, params) => Effect$1.acquireUseRelease(Cache.get(prepareCache, sql), (statement) => {
|
|
32383
|
-
statement.setReturnArrays(true);
|
|
32384
|
-
return runStatementValues(statement, params);
|
|
32385
|
-
}, (statement) => Effect$1.sync(() => statement.setReturnArrays(false)));
|
|
32386
|
-
const runValuesUnprepared = (sql, params) => runStatementValuesUnprepared(db.prepare(sql), params);
|
|
32387
|
-
return identity({
|
|
32388
|
-
execute(sql, params, transformRows) {
|
|
32389
|
-
return transformRows ? Effect$1.map(run(sql, params), transformRows) : run(sql, params);
|
|
32390
|
-
},
|
|
32391
|
-
executeRaw(sql, params) {
|
|
32392
|
-
return run(sql, params, true);
|
|
32393
|
-
},
|
|
32394
|
-
executeValues(sql, params) {
|
|
32395
|
-
return runValues(sql, params);
|
|
32396
|
-
},
|
|
32397
|
-
executeValuesUnprepared(sql, params) {
|
|
32398
|
-
return runValuesUnprepared(sql, params);
|
|
32399
|
-
},
|
|
32400
|
-
executeUnprepared(sql, params, transformRows) {
|
|
32401
|
-
const effect = runStatement(db.prepare(sql), params ?? [], false);
|
|
32402
|
-
return transformRows ? Effect$1.map(effect, transformRows) : effect;
|
|
32403
|
-
},
|
|
32404
|
-
executeStream(_sql, _params) {
|
|
32405
|
-
return Stream$1.die("executeStream not implemented");
|
|
32406
|
-
},
|
|
32407
|
-
backup(destination) {
|
|
32408
|
-
return Effect$1.suspend(() => {
|
|
32409
|
-
let totalPages = 0;
|
|
32410
|
-
return Effect$1.tryPromise({
|
|
32411
|
-
try: () => backup(db, destination, { progress: (progress) => {
|
|
32412
|
-
totalPages = progress.totalPages;
|
|
32413
|
-
} }).then((pages) => ({
|
|
32414
|
-
totalPages: totalPages || pages,
|
|
32415
|
-
remainingPages: 0
|
|
32416
|
-
})),
|
|
32417
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
|
|
32418
|
-
});
|
|
32419
|
-
});
|
|
32420
|
-
},
|
|
32421
|
-
loadExtension(path) {
|
|
32422
|
-
return Effect$1.acquireUseRelease(Effect$1.sync(() => db.enableLoadExtension(true)), () => Effect$1.try({
|
|
32423
|
-
try: () => db.loadExtension(path),
|
|
32424
|
-
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
|
|
32425
|
-
}), () => Effect$1.sync(() => db.enableLoadExtension(false)));
|
|
32426
|
-
}
|
|
32427
|
-
});
|
|
32428
|
-
});
|
|
32429
|
-
const semaphore = yield* Semaphore$1.make(1);
|
|
32430
|
-
const connection = yield* makeConnection;
|
|
32431
|
-
const acquirer = semaphore.withPermits(1)(Effect$1.succeed(connection));
|
|
32432
|
-
const transactionAcquirer = Effect$1.uninterruptibleMask((restore) => {
|
|
32433
|
-
const fiber = Fiber.getCurrent();
|
|
32434
|
-
const scope = Context$1.getUnsafe(fiber.context, Scope.Scope);
|
|
32435
|
-
return Effect$1.as(Effect$1.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection);
|
|
32436
|
-
});
|
|
32437
|
-
return Object.assign(yield* Client.make({
|
|
32438
|
-
acquirer,
|
|
32439
|
-
compiler,
|
|
32440
|
-
transactionAcquirer,
|
|
32441
|
-
beginTransaction: "BEGIN IMMEDIATE",
|
|
32442
|
-
spanAttributes: [...options.spanAttributes ? Object.entries(options.spanAttributes) : [], [ATTR_DB_SYSTEM_NAME, "sqlite"]],
|
|
32443
|
-
transformRows
|
|
32444
|
-
}), {
|
|
32445
|
-
[TypeId$1]: TypeId$1,
|
|
32446
|
-
config: options,
|
|
32447
|
-
backup: (destination) => Effect$1.flatMap(acquirer, (_) => _.backup(destination)),
|
|
32448
|
-
loadExtension: (path) => Effect$1.flatMap(acquirer, (_) => _.loadExtension(path))
|
|
32449
|
-
});
|
|
32450
|
-
});
|
|
32451
|
-
/**
|
|
32452
|
-
* Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
|
|
32453
|
-
*
|
|
32454
|
-
* @category layers
|
|
32455
|
-
* @since 4.0.0
|
|
32456
|
-
*/
|
|
32457
|
-
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));
|
|
32458
|
-
const classifyError = (cause, message, operation) => classifySqliteError(sqliteCauseWithErrno(cause), {
|
|
32459
|
-
message,
|
|
32460
|
-
operation
|
|
32461
|
-
});
|
|
32462
|
-
const sqliteCauseWithErrno = (cause) => {
|
|
32463
|
-
if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) return cause;
|
|
32464
|
-
const errcode = cause.errcode;
|
|
32465
|
-
if (typeof errcode !== "number") return cause;
|
|
32466
|
-
return Object.assign(cause, { errno: errcode });
|
|
32467
|
-
};
|
|
32468
|
-
//#endregion
|
|
32469
32793
|
//#region ../contracts/dist/runtime/index.js
|
|
32470
32794
|
const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
|
|
32471
32795
|
const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
|
|
@@ -32499,6 +32823,7 @@ const scalarKind = (descriptor) => {
|
|
|
32499
32823
|
case "date":
|
|
32500
32824
|
case "integer":
|
|
32501
32825
|
case "number": return "number";
|
|
32826
|
+
case "userId":
|
|
32502
32827
|
case "id":
|
|
32503
32828
|
case "string": return "string";
|
|
32504
32829
|
case "literal": return scalarValueKind(descriptor.value);
|
|
@@ -32597,7 +32922,9 @@ const IndexPointInvalidation = Schema.Struct({
|
|
|
32597
32922
|
index: IndexIdentity,
|
|
32598
32923
|
key: EncodedIndexKey
|
|
32599
32924
|
});
|
|
32925
|
+
const IdDependency = Schema.Struct({ type: Schema.Literal("Id") });
|
|
32600
32926
|
const ReadDependency = Schema.Union([
|
|
32927
|
+
IdDependency,
|
|
32601
32928
|
TableDependency,
|
|
32602
32929
|
DocumentDependency,
|
|
32603
32930
|
IndexRangeDependency
|
|
@@ -32662,7 +32989,8 @@ const InvocationBase = {
|
|
|
32662
32989
|
generation: DeploymentGeneration,
|
|
32663
32990
|
invocationKey: InvocationKey,
|
|
32664
32991
|
function: FunctionAddress,
|
|
32665
|
-
args: Schema.Json
|
|
32992
|
+
args: Schema.Json,
|
|
32993
|
+
identity: Schema.optional(SessionState)
|
|
32666
32994
|
};
|
|
32667
32995
|
const QueryInvocation = Schema.Struct({
|
|
32668
32996
|
...InvocationBase,
|
|
@@ -32685,6 +33013,7 @@ var RuntimeDeploymentUnavailable = class extends Schema.TaggedError()("RuntimeDe
|
|
|
32685
33013
|
message: Schema.String
|
|
32686
33014
|
}) {};
|
|
32687
33015
|
const RuntimeInvocationRejectionCode = Schema.Literals([
|
|
33016
|
+
"SessionExpired",
|
|
32688
33017
|
"FunctionUnavailable",
|
|
32689
33018
|
"InvalidArguments",
|
|
32690
33019
|
"InvocationIdConflict",
|
|
@@ -32710,7 +33039,7 @@ Schema.Union([
|
|
|
32710
33039
|
RuntimeInvocationRejected,
|
|
32711
33040
|
RuntimeInvocationUnavailable
|
|
32712
33041
|
]);
|
|
32713
|
-
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([
|
|
32714
33043
|
dependency.type,
|
|
32715
33044
|
dependency.tableId,
|
|
32716
33045
|
dependency.id
|
|
@@ -32729,11 +33058,17 @@ const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make
|
|
|
32729
33058
|
functionAddress,
|
|
32730
33059
|
args
|
|
32731
33060
|
]));
|
|
32732
|
-
const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args) => encodeCanonicalJson([
|
|
33061
|
+
const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args, sessionEpoch) => encodeCanonicalJson(sessionEpoch === void 0 ? [
|
|
32733
33062
|
deploymentId,
|
|
32734
33063
|
functionKind,
|
|
32735
33064
|
functionAddress,
|
|
32736
33065
|
args
|
|
33066
|
+
] : [
|
|
33067
|
+
deploymentId,
|
|
33068
|
+
functionKind,
|
|
33069
|
+
functionAddress,
|
|
33070
|
+
args,
|
|
33071
|
+
sessionEpoch
|
|
32737
33072
|
]);
|
|
32738
33073
|
const tableDependency = (tableId) => ({
|
|
32739
33074
|
type: "Table",
|
|
@@ -32758,78 +33093,6 @@ const indexPointInvalidation = (tableId, index, key) => ({
|
|
|
32758
33093
|
key
|
|
32759
33094
|
});
|
|
32760
33095
|
//#endregion
|
|
32761
|
-
//#region ../runtime/dist/functions.js
|
|
32762
|
-
var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
|
|
32763
|
-
//#endregion
|
|
32764
|
-
//#region ../contracts/dist/runtime/transport.js
|
|
32765
|
-
const runtimeInvocationPath = "/v1/invoke";
|
|
32766
|
-
const runtimeRevisionPath = "/v1/revision";
|
|
32767
|
-
const runtimeIndexPreparePath = "/v1/indexes/prepare";
|
|
32768
|
-
const runtimeIndexCommitPath = "/v1/indexes/commit";
|
|
32769
|
-
const runtimeFileEffectsPath = "/v1/files/effects";
|
|
32770
|
-
const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
|
|
32771
|
-
const ignotumPathPrefix$1 = "/_ignotum";
|
|
32772
|
-
const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
|
|
32773
|
-
const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
|
|
32774
|
-
const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
|
|
32775
|
-
const RuntimeRequestPath = Schema.Literals([
|
|
32776
|
-
runtimeInvocationPath,
|
|
32777
|
-
runtimeRevisionPath,
|
|
32778
|
-
runtimeIndexPreparePath,
|
|
32779
|
-
runtimeIndexCommitPath,
|
|
32780
|
-
runtimeFileEffectsPath,
|
|
32781
|
-
runtimeFileEffectsAcknowledgePath
|
|
32782
|
-
]);
|
|
32783
|
-
const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
|
|
32784
|
-
const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
|
|
32785
|
-
Schema.Struct({
|
|
32786
|
-
appId: AppId,
|
|
32787
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32788
|
-
nonce: RuntimeRequestNonce,
|
|
32789
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32790
|
-
signature: RuntimeRequestSignature
|
|
32791
|
-
});
|
|
32792
|
-
Schema.Struct({
|
|
32793
|
-
method: Schema.Literal("POST"),
|
|
32794
|
-
path: RuntimeRequestPath,
|
|
32795
|
-
timestamp: RuntimeRequestTimestamp,
|
|
32796
|
-
nonce: RuntimeRequestNonce,
|
|
32797
|
-
appId: AppId,
|
|
32798
|
-
deploymentId: Schema.optional(DeploymentId),
|
|
32799
|
-
bodySha256: Sha256
|
|
32800
|
-
});
|
|
32801
|
-
Schema.Struct({
|
|
32802
|
-
appId: AppId,
|
|
32803
|
-
deploymentId: DeploymentId
|
|
32804
|
-
});
|
|
32805
|
-
Schema.Struct({ completed: Schema.Literal(true) });
|
|
32806
|
-
const RuntimeErrorCode = Schema.Literals([
|
|
32807
|
-
"InvalidRequest",
|
|
32808
|
-
"InvalidSignature",
|
|
32809
|
-
"RequestExpired",
|
|
32810
|
-
"RequestReplay",
|
|
32811
|
-
"InvocationRejected",
|
|
32812
|
-
"RuntimeUnavailable"
|
|
32813
|
-
]);
|
|
32814
|
-
Schema.Struct({
|
|
32815
|
-
code: RuntimeErrorCode,
|
|
32816
|
-
message: Schema.String,
|
|
32817
|
-
rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
|
|
32818
|
-
});
|
|
32819
|
-
//#endregion
|
|
32820
|
-
//#region ../shared/dist/http-body.js
|
|
32821
|
-
const joinChunks = (chunks) => {
|
|
32822
|
-
const byteLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
32823
|
-
const bytes = new Uint8Array(byteLength);
|
|
32824
|
-
let offset = 0;
|
|
32825
|
-
for (const chunk of chunks) {
|
|
32826
|
-
bytes.set(chunk, offset);
|
|
32827
|
-
offset += chunk.byteLength;
|
|
32828
|
-
}
|
|
32829
|
-
return bytes;
|
|
32830
|
-
};
|
|
32831
|
-
const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => Stream.mapError(stream, (error) => error).pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.runCollect, Effect.map(joinChunks)));
|
|
32832
|
-
//#endregion
|
|
32833
33096
|
//#region ../runtime/dist/sync.js
|
|
32834
33097
|
const makeDependencyIndex = () => {
|
|
32835
33098
|
const dependenciesByQuery = MutableHashMap.empty();
|
|
@@ -32900,6 +33163,462 @@ const makeDependencyIndex = () => {
|
|
|
32900
33163
|
};
|
|
32901
33164
|
};
|
|
32902
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));
|
|
32903
33622
|
const syncPath = appSyncPath;
|
|
32904
33623
|
const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
|
|
32905
33624
|
//#endregion
|
|
@@ -33593,14 +34312,49 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
|
|
|
33593
34312
|
}));
|
|
33594
34313
|
};
|
|
33595
34314
|
//#endregion
|
|
33596
|
-
//#region src/dev-runtime/id.ts
|
|
33597
|
-
const idGeneratorLayer = IdGenerator.layer;
|
|
33598
|
-
//#endregion
|
|
33599
34315
|
//#region src/dev-runtime/functions.ts
|
|
34316
|
+
const loadDevelopmentEnvironment = Effect.fn("DevelopmentEnvironment.load")(function* (server, appDirectory) {
|
|
34317
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
34318
|
+
const definitionPath = (yield* Path$1.Path).join(appDirectory, "server", "env.ts");
|
|
34319
|
+
if (!(yield* fileSystem.exists(definitionPath))) return yield* loadEnvironment(appDirectory, emptyEnv, false);
|
|
34320
|
+
const loaded = yield* Effect.tryPromise({
|
|
34321
|
+
try: () => server.ssrLoadModule(normalizePath(definitionPath)),
|
|
34322
|
+
catch: () => EnvironmentConfigurationInvalid.make({
|
|
34323
|
+
message: "Ignotum could not load server/env.ts.",
|
|
34324
|
+
path: definitionPath
|
|
34325
|
+
})
|
|
34326
|
+
});
|
|
34327
|
+
if (!Predicate.isObject(loaded.default) || !isDefinedEnv(loaded.default)) return yield* EnvironmentConfigurationInvalid.make({
|
|
34328
|
+
message: "The default export from server/env.ts is not an Ignotum environment.",
|
|
34329
|
+
path: definitionPath
|
|
34330
|
+
});
|
|
34331
|
+
return yield* loadEnvironment(appDirectory, loaded.default, true);
|
|
34332
|
+
});
|
|
34333
|
+
var DevelopmentEnvironment = class DevelopmentEnvironment extends Context.Service()("ignotum/dev-runtime/functions/DevelopmentEnvironment") {
|
|
34334
|
+
static layer(server, appDirectory) {
|
|
34335
|
+
return Layer.effect(DevelopmentEnvironment, Effect.gen(function* () {
|
|
34336
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
34337
|
+
const path = yield* Path$1.Path;
|
|
34338
|
+
const load = loadDevelopmentEnvironment(server, appDirectory).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path));
|
|
34339
|
+
const initial = yield* load;
|
|
34340
|
+
const current = yield* Ref.make(Result.succeed(initial));
|
|
34341
|
+
const fromResult = Result.match({
|
|
34342
|
+
onFailure: Effect.fail,
|
|
34343
|
+
onSuccess: Effect.succeed
|
|
34344
|
+
});
|
|
34345
|
+
const reload = load.pipe(Effect.result, Effect.flatMap((result) => Ref.set(current, result).pipe(Effect.andThen(fromResult(result)))), Effect.asVoid);
|
|
34346
|
+
return DevelopmentEnvironment.of({
|
|
34347
|
+
reload,
|
|
34348
|
+
snapshot: Ref.get(current).pipe(Effect.flatMap(fromResult))
|
|
34349
|
+
});
|
|
34350
|
+
}));
|
|
34351
|
+
}
|
|
34352
|
+
};
|
|
33600
34353
|
var FunctionRegistry = class FunctionRegistry extends Context.Service()("ignotum/dev-runtime/functions/FunctionRegistry") {
|
|
33601
34354
|
static layer(server, appDirectory) {
|
|
33602
34355
|
return Layer.effect(FunctionRegistry, Effect.gen(function* () {
|
|
33603
34356
|
const path = yield* Path$1.Path;
|
|
34357
|
+
const developmentEnvironment = yield* DevelopmentEnvironment;
|
|
33604
34358
|
return FunctionRegistry.of({ resolve: Effect.fn("FunctionRegistry.resolve")(function* (functionAddress, kind, args) {
|
|
33605
34359
|
const { functionName, moduleName } = apiFunctionParts(functionAddress);
|
|
33606
34360
|
const modulePath = normalizePath(path.join(appDirectory, "server", `${moduleName}.ts`));
|
|
@@ -33618,6 +34372,11 @@ var FunctionRegistry = class FunctionRegistry extends Context.Service()("ignotum
|
|
|
33618
34372
|
message: `Unknown server function ${functionAddress}.`
|
|
33619
34373
|
});
|
|
33620
34374
|
const definition = inspected.definition;
|
|
34375
|
+
if (getFunctionEnvironment(candidate) === void 0) return yield* FunctionUnavailable.make({
|
|
34376
|
+
cause: /* @__PURE__ */ new Error(`${functionAddress} has no environment binding.`),
|
|
34377
|
+
function: functionAddress,
|
|
34378
|
+
message: `Could not load ${functionAddress}.`
|
|
34379
|
+
});
|
|
33621
34380
|
if (definition._tag !== kind) return yield* WrongFunctionKind.make({
|
|
33622
34381
|
actual: definition._tag,
|
|
33623
34382
|
expected: kind,
|
|
@@ -33630,6 +34389,11 @@ var FunctionRegistry = class FunctionRegistry extends Context.Service()("ignotum
|
|
|
33630
34389
|
message: `Invalid arguments for ${functionAddress}.`
|
|
33631
34390
|
}))),
|
|
33632
34391
|
definition,
|
|
34392
|
+
environment: (yield* developmentEnvironment.snapshot.pipe(Effect.mapError((cause) => FunctionUnavailable.make({
|
|
34393
|
+
cause,
|
|
34394
|
+
function: functionAddress,
|
|
34395
|
+
message: `Could not load ${functionAddress}.`
|
|
34396
|
+
})))).value,
|
|
33633
34397
|
schema: inspected.schema
|
|
33634
34398
|
};
|
|
33635
34399
|
}) });
|
|
@@ -33664,8 +34428,17 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
33664
34428
|
dates
|
|
33665
34429
|
};
|
|
33666
34430
|
}));
|
|
33667
|
-
return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved) {
|
|
33668
|
-
|
|
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
|
+
}));
|
|
34437
|
+
const invoke = (context) => Effect.gen(() => resolved.definition.handler(Object.freeze({
|
|
34438
|
+
...context,
|
|
34439
|
+
id,
|
|
34440
|
+
env: resolved.environment ?? Object.freeze({})
|
|
34441
|
+
}), resolved.args)).pipe(Effect.matchEffect({
|
|
33669
34442
|
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
33670
34443
|
onSuccess: (value) => encodeSuccess(resolved.definition, value)
|
|
33671
34444
|
}));
|
|
@@ -33674,7 +34447,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
33674
34447
|
const result = {
|
|
33675
34448
|
type: "Query",
|
|
33676
34449
|
result: value,
|
|
33677
|
-
dependencies,
|
|
34450
|
+
dependencies: readId ? [...dependencies, { type: "Id" }] : dependencies,
|
|
33678
34451
|
observedRevision
|
|
33679
34452
|
};
|
|
33680
34453
|
return files.length === 0 ? result : {
|
|
@@ -33713,7 +34486,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
33713
34486
|
return {
|
|
33714
34487
|
type: "Query",
|
|
33715
34488
|
result,
|
|
33716
|
-
dependencies: [],
|
|
34489
|
+
dependencies: readId ? [{ type: "Id" }] : [],
|
|
33717
34490
|
observedRevision: yield* database.revision
|
|
33718
34491
|
};
|
|
33719
34492
|
})));
|
|
@@ -33724,9 +34497,9 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
|
|
|
33724
34497
|
const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function* () {
|
|
33725
34498
|
const registry = yield* FunctionRegistry;
|
|
33726
34499
|
const executor = yield* FunctionExecutor;
|
|
33727
|
-
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) {
|
|
33728
34501
|
const resolved = yield* registry.resolve(functionAddress, kind, args);
|
|
33729
|
-
return { execute: executor.execute(functionAddress, kind, resolved) };
|
|
34502
|
+
return { execute: executor.execute(functionAddress, kind, resolved, identity) };
|
|
33730
34503
|
}) });
|
|
33731
34504
|
}));
|
|
33732
34505
|
//#endregion
|
|
@@ -33752,7 +34525,7 @@ const validPng = (bytes) => bytes.length >= 20 && startsWith(bytes, [
|
|
|
33752
34525
|
10,
|
|
33753
34526
|
26,
|
|
33754
34527
|
10
|
|
33755
|
-
]) && ascii(bytes.subarray(bytes.length -
|
|
34528
|
+
]) && ascii(bytes.subarray(bytes.length - 8, bytes.length - 4)) === "IEND";
|
|
33756
34529
|
const validGif = (bytes) => bytes.length >= 14 && (ascii(bytes.subarray(0, 6)) === "GIF87a" || ascii(bytes.subarray(0, 6)) === "GIF89a") && bytes.at(-1) === 59;
|
|
33757
34530
|
const validWebp = (bytes) => {
|
|
33758
34531
|
if (bytes.length < 12 || ascii(bytes.subarray(0, 4)) !== "RIFF" || ascii(bytes.subarray(8, 12)) !== "WEBP") return false;
|
|
@@ -33985,18 +34758,21 @@ const localSyncIdentity = {
|
|
|
33985
34758
|
deploymentId: DeploymentId.make("dep_000000000000000000000001"),
|
|
33986
34759
|
generation: DeploymentGeneration.make(0)
|
|
33987
34760
|
};
|
|
34761
|
+
const reloadEnvironmentAndInvalidate = (reload, publish) => reload.pipe(Effect.ensuring(publish));
|
|
33988
34762
|
const queryInvalidationLayer = Layer.effect(QueryInvalidation, Effect.gen(function* () {
|
|
34763
|
+
const cache = yield* LocalQueryCache;
|
|
33989
34764
|
const pubsub = yield* PubSub.unbounded();
|
|
33990
34765
|
const latestRevision = yield* Ref.make(AppStateRevision.make(0));
|
|
33991
34766
|
return QueryInvalidation.of({
|
|
33992
34767
|
latestRevision: Ref.get(latestRevision),
|
|
33993
34768
|
publish: (event) => Effect.gen(function* () {
|
|
34769
|
+
cache.invalidate(event);
|
|
33994
34770
|
if (event.type === "Dependencies") yield* Ref.update(latestRevision, (current) => AppStateRevision.make(Math.max(current, event.committedRevision)));
|
|
33995
34771
|
yield* PubSub.publish(pubsub, event);
|
|
33996
34772
|
}).pipe(Effect.asVoid),
|
|
33997
34773
|
subscribe: PubSub.subscribe(pubsub)
|
|
33998
34774
|
});
|
|
33999
|
-
}));
|
|
34775
|
+
})).pipe(Layer.provideMerge(LocalQueryCache.layer));
|
|
34000
34776
|
var InvocationIdConflict = class extends Schema.TaggedError()("InvocationIdConflict", { id: Schema.String }) {};
|
|
34001
34777
|
const MutationReplayRow = Schema.Struct({
|
|
34002
34778
|
input: Schema.String,
|
|
@@ -34052,10 +34828,17 @@ const syncError = (code, message, operation) => {
|
|
|
34052
34828
|
operation
|
|
34053
34829
|
};
|
|
34054
34830
|
};
|
|
34055
|
-
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
|
+
};
|
|
34056
34838
|
const runtime = yield* FunctionRuntime;
|
|
34057
34839
|
const invalidation = yield* QueryInvalidation;
|
|
34058
34840
|
const mutationReplay = yield* MutationReplay;
|
|
34841
|
+
const queryCache = yield* LocalQueryCache;
|
|
34059
34842
|
const files = yield* LocalApplicationFiles;
|
|
34060
34843
|
const subscriptions = yield* Ref.make(HashMap.empty());
|
|
34061
34844
|
const dependencyIndex = makeDependencyIndex();
|
|
@@ -34064,7 +34847,11 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34064
34847
|
const writeSemaphore = yield* Semaphore.make(1);
|
|
34065
34848
|
const write = yield* socket.writer;
|
|
34066
34849
|
const send = Effect.fn("SyncServer.send")(function* (message) {
|
|
34067
|
-
|
|
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)));
|
|
34068
34855
|
});
|
|
34069
34856
|
const sendResolutionError = (operation, error, deliver = send) => {
|
|
34070
34857
|
const code = error._tag;
|
|
@@ -34080,7 +34867,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34080
34867
|
let prepared = previouslyPrepared;
|
|
34081
34868
|
while (yield* isActive(subscriptionId, subscription)) {
|
|
34082
34869
|
if (prepared === void 0) {
|
|
34083
|
-
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({
|
|
34084
34871
|
FunctionUnavailable: (error) => sendResolutionError(operation, error, deliver),
|
|
34085
34872
|
InvalidArguments: (error) => sendResolutionError(operation, error, deliver),
|
|
34086
34873
|
UnknownFunction: (error) => sendResolutionError(operation, error, deliver),
|
|
@@ -34089,7 +34876,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34089
34876
|
if (resolved === void 0) return;
|
|
34090
34877
|
prepared = resolved;
|
|
34091
34878
|
}
|
|
34092
|
-
const result = yield* prepared.execute;
|
|
34879
|
+
const result = yield* queryCache.execute(subscription.queryKey, identity, yield* invalidation.latestRevision, prepared.execute);
|
|
34093
34880
|
prepared = void 0;
|
|
34094
34881
|
if (result.type !== "Query") return;
|
|
34095
34882
|
if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
|
|
@@ -34100,7 +34887,12 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34100
34887
|
type: "Snapshot",
|
|
34101
34888
|
id: subscriptionId,
|
|
34102
34889
|
result: result.result,
|
|
34103
|
-
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
|
+
}
|
|
34104
34896
|
};
|
|
34105
34897
|
yield* deliver(granted.length === 0 ? snapshot : {
|
|
34106
34898
|
...snapshot,
|
|
@@ -34118,6 +34910,17 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34118
34910
|
}))), FiberSet.run(invocationFibers), Effect.asVoid);
|
|
34119
34911
|
};
|
|
34120
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
|
+
}
|
|
34121
34924
|
const current = yield* Ref.get(subscriptions);
|
|
34122
34925
|
yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) => scheduleQuery(subscriptionId, subscription));
|
|
34123
34926
|
});
|
|
@@ -34141,7 +34944,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34141
34944
|
yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
|
|
34142
34945
|
return;
|
|
34143
34946
|
}
|
|
34144
|
-
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({
|
|
34145
34948
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
34146
34949
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
34147
34950
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
@@ -34166,7 +34969,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34166
34969
|
yield* files.releasePreparation(message.id);
|
|
34167
34970
|
return;
|
|
34168
34971
|
}
|
|
34169
|
-
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({
|
|
34170
34973
|
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
34171
34974
|
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
34172
34975
|
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
@@ -34176,7 +34979,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34176
34979
|
yield* files.releasePreparation(message.id);
|
|
34177
34980
|
return;
|
|
34178
34981
|
}
|
|
34179
|
-
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
|
|
34982
|
+
const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args, identity.sessionEpoch);
|
|
34180
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) => {
|
|
34181
34984
|
if (!("committedRevision" in result)) return send({
|
|
34182
34985
|
type: "Result",
|
|
@@ -34233,18 +35036,23 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
|
|
|
34233
35036
|
});
|
|
34234
35037
|
yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
|
|
34235
35038
|
type: "Handshake",
|
|
34236
|
-
...localSyncIdentity
|
|
35039
|
+
...localSyncIdentity,
|
|
35040
|
+
session: identity,
|
|
35041
|
+
serverTime: yield* Clock.currentTimeMillis
|
|
34237
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)));
|
|
34238
35043
|
});
|
|
34239
35044
|
var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
|
|
34240
35045
|
const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
34241
35046
|
const sqliteLayer = layer({ filename: databasePath });
|
|
34242
|
-
const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(
|
|
34243
|
-
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(
|
|
34244
|
-
const
|
|
34245
|
-
const
|
|
35047
|
+
const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(IdGenerator.layer, sqliteLayer)));
|
|
35048
|
+
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(IdGenerator.layer));
|
|
35049
|
+
const environmentLayer = DevelopmentEnvironment.layer(server, appDirectory).pipe(Layer.provide(layer$1), Layer.orDie);
|
|
35050
|
+
const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(Layer.merge(layer$1, environmentLayer))), executorLayer)));
|
|
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))));
|
|
34246
35052
|
return Layer.effect(SyncHandlers, Effect.gen(function* () {
|
|
35053
|
+
const localId = yield* LocalId;
|
|
34247
35054
|
const invalidation = yield* QueryInvalidation;
|
|
35055
|
+
const environment = yield* DevelopmentEnvironment;
|
|
34248
35056
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
34249
35057
|
const path = yield* Path$1.Path;
|
|
34250
35058
|
const scope = yield* Effect.scope;
|
|
@@ -34257,7 +35065,49 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
34257
35065
|
}));
|
|
34258
35066
|
const httpApp = Effect.gen(function* () {
|
|
34259
35067
|
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
34260
|
-
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
|
+
}
|
|
34261
35111
|
if (pathname.startsWith(fileUploadUrlPrefix)) {
|
|
34262
35112
|
if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
|
|
34263
35113
|
const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
|
|
@@ -34291,17 +35141,29 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
|
|
|
34291
35141
|
}, { status: pathname === syncPath ? 426 : 404 });
|
|
34292
35142
|
});
|
|
34293
35143
|
const socketApp = Effect.gen(function* () {
|
|
34294
|
-
const
|
|
34295
|
-
|
|
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);
|
|
34296
35151
|
return HttpServerResponse.empty();
|
|
34297
35152
|
});
|
|
34298
35153
|
const http = yield* makeHandler(httpApp, { scope });
|
|
34299
35154
|
const upgrade = yield* makeUpgradeHandler(Effect.succeed(webSocketServer), socketApp, { scope });
|
|
34300
35155
|
const serverDirectory = path.join(appDirectory, "server");
|
|
35156
|
+
const environmentPath = path.join(appDirectory, ".env.ignotum");
|
|
34301
35157
|
const reloadUnsafe = Effect.fn("SyncServer.reload")(function* (file, event) {
|
|
35158
|
+
const isEnvironmentValues = path.resolve(file) === path.resolve(environmentPath);
|
|
34302
35159
|
const relative = path.relative(serverDirectory, file);
|
|
34303
|
-
|
|
34304
|
-
if (
|
|
35160
|
+
const isServerTypeScript = !relative.startsWith("..") && !path.isAbsolute(relative) && file.endsWith(".ts");
|
|
35161
|
+
if (!isEnvironmentValues && !isServerTypeScript) return;
|
|
35162
|
+
if (isServerTypeScript && event !== "change") yield* generate(appDirectory).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path));
|
|
35163
|
+
if (isEnvironmentValues || relative === "env.ts") {
|
|
35164
|
+
yield* reloadEnvironmentAndInvalidate(environment.reload, invalidation.publish({ type: "All" }));
|
|
35165
|
+
return;
|
|
35166
|
+
}
|
|
34305
35167
|
yield* invalidation.publish({ type: "All" });
|
|
34306
35168
|
});
|
|
34307
35169
|
const reload = (file, event) => reloadSemaphore.withPermits(1)(reloadUnsafe(file, event)).pipe(Effect.catchCause((cause) => Effect.logError("Could not reload Ignotum server functions.").pipe(Effect.annotateLogs({
|
|
@@ -34462,42 +35324,55 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
|
|
|
34462
35324
|
}));
|
|
34463
35325
|
});
|
|
34464
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
|
|
34465
35336
|
//#region ../../docs/agent/AGENTS.md?raw
|
|
34466
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";
|
|
34467
35338
|
//#endregion
|
|
34468
35339
|
//#region ../../docs/agent/skills/ignotum/SKILL.md?raw
|
|
34469
|
-
var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing a deployed app's fields, check\n[Deployment model](references/deployment-model.md). After the first\nactivation, hosted deployment requires the same table names, field names, and field validators.\nIndex-only changes are allowed. Do not promise that adding an optional field is compatible.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
|
|
35340
|
+
var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Declare or use server environment variables | [Environment variables](references/environment-variables.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define server configuration only in `server/env.ts` with `defineEnv` and supply it from the\n project-root `.env.ignotum`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing a deployed app's fields, check\n[Deployment model](references/deployment-model.md). After the first\nactivation, hosted deployment requires the same table names, field names, and field validators.\nIndex-only changes are allowed. Do not promise that adding an optional field is compatible.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
|
|
34470
35341
|
//#endregion
|
|
34471
35342
|
//#region src/cli/agent-files.ts
|
|
34472
35343
|
const references = [
|
|
35344
|
+
["authentication.md", authentication_default],
|
|
35345
|
+
["profile-sharing.md", profile_sharing_default],
|
|
35346
|
+
["local-authentication.md", local_authentication_default],
|
|
34473
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"],
|
|
34474
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"],
|
|
34475
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"],
|
|
34476
35350
|
["database.md", "---\ntitle: Database overview\nlabel: Overview\ndescription: Understand the persistent data model in an Ignotum app.\nsection: Database\nslug: database\norder: 10\n---\n\n# Database overview\n\nEach Ignotum app has a database described by `server/schema.ts`. A schema contains tables, stored\nfields, and indexes. Every stored document also has a table-specific ID and creation and update\ntimestamps.\n\nQueries and mutations read through `ctx.db`. Only mutations can insert, patch, replace, or delete\ndocuments. All reads and writes in one mutation commit together.\n\n- [Schema](schema.md) defines tables, fields, IDs, and schema changes.\n- [Values](values.md) lists the validators used for stored fields and function contracts.\n- [Reading data](reading-data.md) covers document and table reads.\n- [Writing data](writing-data.md) covers database changes and transaction behavior.\n- [Indexes](indexes.md) covers index definitions, ranges, and ordering.\n- [Pagination](pagination.md) covers cursor-based database and Web pagination.\n\nNext: [Schema](schema.md)\n"],
|
|
34477
35351
|
["deploy-app.md", "---\ntitle: Deploy an app\ndescription: Link an app and publish a deployment to Ignotum Cloud.\nsection: Deployments\nslug: deploy\norder: 51\n---\n\n# Deploy an app\n\nSign in through the browser device flow:\n\n```sh\nnpx ignotum auth login\n```\n\nThe command opens `auth.ignotum.cloud`, asks you to sign in with GitHub, and requires you to approve\nthe code shown in the terminal. The CLI stores the resulting session under your user config\ndirectory with owner-only file permissions. It never writes the session into the app directory.\nIgnotum Cloud accounts are currently invite-only; the email returned by GitHub must have a pending\nplatform invite before the first sign-in.\n\n## First deployment\n\nChoose the app slug on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nSlugs use lowercase letters, numbers, and hyphens. They are 1 through 63 characters, cannot start\nor end with a hyphen, and must be globally available. The deployed app uses\n`https://my-app.ignotum.app`.\n\nIf the slug already belongs to your account, the CLI links the local directory to that app. If it\nis available, the CLI creates the app. If you omit `--app`, it asks for a slug in an interactive\nterminal.\n\nThe CLI writes the app ID, slug, and API URL to `.ignotum/app.json`. The generated `.gitignore`\nexcludes `.ignotum`.\n\n## Later deployments\n\nOnce linked, run:\n\n```sh\nnpx ignotum deploy\n```\n\nThe command updates generated code, builds and validates the complete app, uploads a new\ndeployment, and activates it. There is no separate public build command.\n\nThe active deployment stays unchanged if any of these steps fail. See\n[Deployment model](deployment-model.md) for activation, schema compatibility, and interrupted\ndeployments.\n\nNext: [Deployment model](deployment-model.md)\n"],
|
|
34478
|
-
["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command.
|
|
34479
|
-
["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema, and
|
|
35352
|
+
["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\nThe deployment also captures `.env.ignotum`; later edits do not change an existing deployment.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command. An older deployment retains its original environment if it is\nreactivated through the HTTP API. Deploying older source again creates a new deployment with the\ncurrent `.env.ignotum` values.\n\n## Schema compatibility\n\nThe first active deployment fixes the stored table and field schema. Later deployments may change\nWeb code, functions, and indexes. Changing a table name, adding or removing a stored field, or\nchanging a field validator causes activation to fail with `SchemaIncompatible`. The active\ndeployment and stored data remain unchanged.\n\nAdding, removing, renaming, or reordering indexes is supported. Ignotum prepares new indexes before\nthe new deployment starts accepting mutations.\n\n## Failed and interrupted deployments\n\nA build, upload, validation, compatibility, or activation failure leaves the current deployment\nactive.\n\nIf an upload or finalization request loses its connection, the CLI prints the new deployment ID.\nUnfinished uploads expire after 24 hours. If activation cannot be confirmed, use the\n[HTTP API](api.md) to inspect the app's `activeDeploymentId`, or run `ignotum deploy` again to create\nand activate a new deployment.\n\nSee [Guarantees](guarantees.md) for the activation contract and [Limits](limits.md) for deployment\nretention.\n"],
|
|
35353
|
+
["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema,\nserver environment snapshot, and public assets. Ignotum activates these parts together after the\ncomplete deployment passes its checks.\n\nUse `ignotum deploy` to link a local project to a hosted app and publish it. A failed build, upload,\nvalidation, or activation does not replace the active deployment.\n\n- [Deploy an app](deploy-app.md) covers the command, first-time linking, and later deployments.\n- [Deployment model](deployment-model.md) covers activation, schema compatibility, and interrupted\n deployments.\n- [Limits](limits.md) lists deployment size and retention limits.\n\nNext: [Deploy an app](deploy-app.md)\n"],
|
|
34480
35354
|
["errors.md", "---\ntitle: Errors\ndescription: Define, return, and handle typed application errors.\nsection: Functions\nslug: errors\norder: 24\n---\n\n# Errors\n\nFunctions can return typed application errors to the client. Ignotum keeps runtime failures and\nthrown exceptions separate from those public errors.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nIn a mutation, an application error rolls back every write from that invocation. In a query, it\nbecomes the settled failure for the subscription at the revision the query observed.\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n\nAn internal failure also rolls back a mutation. It does not become one of the application's\ndeclared errors, even if a thrown value happens to have the same shape.\n\n## Web matching\n\nThe client `Result.match` separates `error` and `internalError`. `error` handles only errors declared\nby the function. `internalError` handles the reserved platform error. If `internalError` is absent,\nmatching that result throws the error to the nearest UI error boundary.\n\nProtocol and connection failures are outside the application `Result`. A mutation promise rejects\nfor an operation-scoped protocol failure. A query hook throws a subscription infrastructure\nfailure during rendering so the UI error boundary can handle it.\n"],
|
|
35355
|
+
["environment-variables.md", "---\ntitle: Environment variables\ndescription: Declare, validate, and read server-only configuration.\nsection: Functions\nslug: environment-variables\norder: 24\n---\n\n# Environment variables\n\nIgnotum provides typed, server-only configuration through `ctx.env`. Declare the variables in\n`server/env.ts` and supply their values in `.env.ignotum` at the project root. The same file is used\nfor local development and deployment.\n\n```ts\nimport { defineEnv } from \"ignotum/server\";\n\nexport default defineEnv(({ values }) => ({\n API_BASE_URL: values.string(),\n API_TOKEN: values.secret(),\n RETRY_LIMIT: values.integer(),\n SERVICE_ACCOUNT: values.secret(\n values.object({\n clientEmail: values.string(),\n privateKey: values.string(),\n }),\n ),\n}));\n```\n\nNames must contain uppercase letters, digits, and underscores, cannot begin with a digit, and cannot\nuse the reserved `IGNOTUM_` prefix.\n\nAdd the matching values to `.env.ignotum`:\n\n```dotenv\nAPI_BASE_URL=https://api.example.com\nAPI_TOKEN=replace-me\nRETRY_LIMIT=3\nSERVICE_ACCOUNT={\"clientEmail\":\"service@example.com\",\"privateKey\":\"replace-me\"}\n```\n\nEach value is tried as a string first. If that does not match its validator, Ignotum parses it as\nJSON and tries again. Numbers, booleans, arrays, records, and objects therefore use JSON syntax. If\na union accepts a string, the original string wins. Empty strings are preserved, and values are not\nread from the process environment.\n\nThe declaration and values must agree exactly. A missing file, missing required variable, extra\nvariable, or invalid value stops development startup or deployment. A `.env.ignotum` file without\n`server/env.ts` is also an error. When neither file exists, `ctx.env` is an empty object.\n\n## Read values in a function\n\nPlain values are available directly. Secret values require an explicit unwrap:\n\n```ts\nimport { Secret } from \"ignotum/server\";\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const status = query({\n returns: values.string(),\n handler: function* (ctx) {\n const token = Secret.value(ctx.env.API_TOKEN);\n return `${ctx.env.API_BASE_URL}:${token.length}`;\n },\n});\n```\n\n`values.secret()` is shorthand for `values.secret(values.string())`. A secret can wrap another\nenvironment validator, including an object. Until it is unwrapped, string conversion, inspection,\nand JSON serialization display `<redacted>` rather than its value. Once application code calls\n`Secret.value`, it is responsible for the plain value and can disclose or store it.\n\nSecret validators are available only inside `defineEnv`. They cannot be used for database fields,\nfunction arguments, results, or application errors.\n\n## Development and deployment\n\nChanging `.env.ignotum` during development reloads the validated environment and refreshes active\nqueries. An invalid edit makes functions unavailable until the file is fixed; Ignotum does not keep\nserving the previous values.\n\nEvery deployment captures an immutable snapshot of `.env.ignotum`. Changing the file requires a\nnew deployment. Reactivating an older deployment restores the environment captured with that\ndeployment.\n\nEnvironment values stay out of browser output and local build files. Hosted values are kept in\nprivate storage encrypted by the cloud provider and are supplied only to the isolated function\nruntime. This Private Alpha release does not provide customer-managed encryption keys or\noperator-blind encryption. Authorized Ignotum platform operators and the cloud storage provider\nremain within the trust boundary.\n\nKeep `.env.ignotum` out of Git. New Ignotum projects ignore `.env.*` and retain the `!.env.example`\nexception, so a value-free `.env.example` can document the required names. Existing projects should\nadd an equivalent ignore rule.\n\nSee [Functions](functions.md), [Values](values.md), and [Limits](limits.md).\n"],
|
|
34481
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"],
|
|
34482
|
-
["functions.md", "---\ntitle: Functions overview\nlabel: Overview\ndescription: Define callable application logic and use its generated API.\nsection: Functions\nslug: functions\norder: 21\n---\n\n# Functions overview\n\nQueries and mutations are callable application functions. Define them in `.ts` files directly\ninside `server`. The file name becomes the generated API module, and each exported function keeps\nits export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nFunction module names must be valid TypeScript identifiers. Ignotum ignores `schema.ts`,\n`index.ts`, files ending in `.test.ts` or `.spec.ts`, and names beginning with `_`. Nested files do\nnot become API modules.\n\n## Definitions\n\nImport schema-bound builders from the generated server file:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix. The generated definitions provide types and runtime validation for\narguments, successful returns, and application errors.\n\nThe `args` object describes one object parameter. Omit it when the function takes no arguments.\nOmit `returns` only when the handler returns `void`. Ignotum does not infer a runtime validator\nfrom a TypeScript return type.\n\nHandlers are generator functions. Use `yield*` for database and `Result` operations, then use an\nordinary `return` for a successful value. Do not mark a handler `async`.\n\n## Generated API\n\nCode generation writes browser references such as `api.todos.list` to `_generated/api.ts`. Web\ncode passes a reference to `useQuery`, `usePaginatedQuery`, or `useMutation`. The reference carries\nthe function's arguments, result, and errors into the call's TypeScript type.\n\n[Queries](queries.md) and [Mutations](mutations.md) cover each complete workflow.\n\n## Runtime boundary\n\nFunctions can import `_generated/server.ts`, `_generated/types.ts`, other server modules, shared\nmodules, and supported dependencies. They cannot import client files, `_generated/api.ts`,\n`ignotum/client`, Node built-ins, native add-ons, or dynamic imports.\n\nHosted functions have no direct network, filesystem, environment, or process access. Use
|
|
34483
|
-
["generated-code.md", "---\ntitle: Generated code\ndescription: Understand generated function references, builders, and data types.\nsection: Development\nslug: generated-code\norder: 61\n---\n\n# Generated code\n\nIgnotum generates the bindings that connect the schema, functions, and Web code. The development\nserver updates them before startup and when the relevant source changes.\n\nRun generation directly before a typecheck or an editor session without the development server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nCode generation writes three files:\n\n| File | Contents |\n| ---------------------- | ------------------------------------------------------- |\n| `_generated/server.ts` | Schema-bound `query`, `mutation`, and `values` exports. |\n| `_generated/api.ts` | Web references such as `api.todos.list`. |\n| `_generated/types.ts` | `DataModel`, `Doc`, and `Id` types. |\n\nDo not edit these files. Ignotum refuses to overwrite a conflicting file that lacks its generated\nheader. Move the conflicting file elsewhere and run `codegen` again.\n\nKeep the `.js` suffix when importing a generated module:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Doc, Id } from \"@/_generated/types.js\";\n```\n\nThe API file changes when function modules or exports change. Server builders and data types change\nwith the schema. See [Functions](functions.md) for function module naming
|
|
34484
|
-
["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"],
|
|
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"],
|
|
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"],
|
|
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"],
|
|
34485
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"],
|
|
34486
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"],
|
|
34487
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"],
|
|
34488
|
-
["limits.md", "---\ntitle: Limits\ndescription: Understand hosted limits for functions, data, connections, and deployments.\nsection: Reference\nslug: limits\norder: 71\n---\n\n# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports sync admission, execution time, result size, and app storage quota failures as\n`ResourceLimitExceeded`. Some lower-level document, collection, memory, stack, or operation limit\nfailures become `InternalServerError` instead. A deployment that exceeds a limit fails before\nactivation, so the current deployment stays active.\n\n## Functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries a query when app data changes while it runs. One logical execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nThe inventory contains at most 512 listed files. Its own `inventory.json` upload is additional.\nRequests with JSON bodies to `api.ignotum.cloud` are also limited to 1 MiB.\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
|
|
34489
|
-
["local-development.md", "---\ntitle: Local development\ndescription: Run an Ignotum app locally and manage its local data.\nsection: Development\nslug: local-development\norder: 60\n---\n\n# Local development\n\nRun the development server from the app directory:\n\n```sh\nnpx ignotum dev\n```\n\nThe command requires `client/index.tsx` and `server/schema.ts`. It updates generated code, starts\nthe Web and function runtimes, and prints the local URL. The default is\n<http://127.0.0.1:3210>.\n\nClient changes use hot reload. Changes to server `.ts` files reload the function modules and\nrefresh active queries. Adding or removing a function module also updates the API references.\n\nThe development server uses one local database for the app directory. A second server for the same\napp fails with a message that identifies the process holding the database lock.\n\n## Options\n\nUse flags to change the listener or open a browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------------------------------------ |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. The command fails if it is in use. |\n| `--open` | off | Open the app in the default browser after startup. |\n\n## Local data\n\nLocal data survives restarts and lives under `.ignotum/dev`. The generated project ignores\n`.ignotum` in Git.\n\nStop the development server before resetting the database:\n\n```sh\nnpx ignotum dev db reset\n```\n\nThe command refuses to reset data while a live development server holds the lock. A successful\nreset removes the local database. The next start creates an empty database from the current schema.\n\nLocal development does not reproduce every hosted resource limit or the hosted function runtime.\nRun the typecheck and review [Limits](limits.md) before deployment.\n\nNext: [Generated code](generated-code.md)\n"],
|
|
34490
|
-
["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"],
|
|
34491
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"],
|
|
34492
|
-
["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
|
|
34493
|
-
["project-structure.md", "---\ntitle: Project structure\ndescription: Place Web code, functions, data definitions, and shared code in an Ignotum app.\nsection: Start\nslug: project-structure\norder: 4\n---\n\n# Project structure\n\nAn Ignotum project separates browser code from function code while keeping both in one app:\n\n```text\nclient/\n index.tsx\nserver/\n schema.ts\n todos.ts\nshared/\n_generated/\npublic/\n```\n\nOnly `client/index.tsx` and `server/schema.ts` are required. The other paths appear when the app\nneeds them.\n\n## `client`\n\n`client` contains the Web part. Its `index.tsx` file default-exports one `app(...)` definition:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nClient modules import browser APIs and UI exports from `ignotum/client`. They may also import\n`_generated/api.ts`, `_generated/types.ts`, other client modules, shared modules, and\nbrowser-compatible dependencies. They cannot import server modules or `ignotum/server`.\n\n## `server`\n\n`server/schema.ts` defines the database schema. Other `.ts` files directly inside `server` may\nexport queries and mutations. Their file names become generated API modules. For example,\n`server/todos.ts` produces references such as `api.todos.list`.\n\nSchema definitions import `defineSchema` from `ignotum/server`. Functions import schema-bound\n`query`, `mutation`, and `values` from `@/_generated/server.js`. Server modules may import other\nserver modules, shared modules, generated server and data types, and supported dependencies. They\ncannot import client modules or `ignotum/client`.\n\n## `shared`\n\n`shared` contains code that is valid in both runtimes. Shared modules cannot depend on browser-only\nor function-only APIs. Put a type, validator-independent helper, or constant here only when both\nsides use it.\n\n## `_generated`\n\nIgnotum writes generated function references, schema-bound builders, and data types to\n`_generated`. Do not edit these files. See [Generated code](generated-code.md).\n\n## `public`\n\nThe optional top-level `public` directory contains fixed files that keep their relative paths as\nWeb URLs. It is separate from application files stored in database documents. See\n[Public assets](public-assets.md).\n\nNext: [Schema](schema.md)\n"],
|
|
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"],
|
|
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"],
|
|
34494
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"],
|
|
34495
|
-
["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"],
|
|
34496
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"],
|
|
34497
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"],
|
|
34498
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"],
|
|
34499
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"],
|
|
34500
|
-
["values.md", "---\ntitle: Values\ndescription: Use runtime validators and their matching TypeScript types.\nsection: Database\nslug: values\norder: 12\n---\n\n# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. The same definition supplies a TypeScript type and a runtime check. Do not substitute a\nTypeScript-only interface where Ignotum asks for a validator.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.file(options?)` | `FileValue` | Accepts configured image formats and a byte limit. See [Files](files.md). |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\nDates cross the Web and function boundary as `Date` instances. Ignotum rejects invalid dates.\n`values.number()` rejects `NaN`, `Infinity`, and `-Infinity`. Use `values.integer()` when fractional\nvalues should also be rejected.\n\n`values.file()` accepts JPEG, PNG, WebP, AVIF, and GIF by default. Pass `formats` to allow a\nnon-empty subset and `maxBytes` to set a positive limit up to 10 MiB. [Files](files.md) covers the\ncomplete file workflow and the places where file values are allowed.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method returns a new validator and leaves the source unchanged. The next method works from the\ncurrent result. Added fields can be picked, omitted, or overridden immediately. An omitted name can\nbe added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\nFixed object validators describe named fields. Use `values.record(value)` when keys are not known\nin advance.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n\nWrapping a validator in both supports omission and an explicit `null`:\n\n```ts\nvalues.optional(values.nullable(values.date()));\n```\n"],
|
|
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"],
|
|
34501
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"],
|
|
34502
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"]
|
|
34503
35378
|
];
|
|
@@ -34987,12 +35862,17 @@ const main = () => {
|
|
|
34987
35862
|
};
|
|
34988
35863
|
//#endregion
|
|
34989
35864
|
//#region src/cli/bin.ts
|
|
34990
|
-
|
|
35865
|
+
const discoveryWorker = process$1.argv[2] === "__ignotum_discover_server";
|
|
35866
|
+
const writeDiscovery = (encoded) => {
|
|
35867
|
+
const bytes = new TextEncoder().encode(encoded);
|
|
35868
|
+
let offset = 0;
|
|
35869
|
+
while (offset < bytes.byteLength) offset += writeSync(3, bytes, offset, bytes.byteLength - offset);
|
|
35870
|
+
};
|
|
35871
|
+
if (discoveryWorker) {
|
|
34991
35872
|
const appDirectory = process$1.argv[3];
|
|
34992
35873
|
const moduleNames = process$1.argv[4];
|
|
34993
|
-
const outputPath = process$1.argv[5];
|
|
34994
35874
|
globalThis.fetch = () => Promise.reject(/* @__PURE__ */ new Error("Network access is disabled during builds."));
|
|
34995
|
-
(appDirectory === void 0 || moduleNames === void 0
|
|
35875
|
+
(appDirectory === void 0 || moduleNames === void 0 ? Effect.die("Invalid isolated discovery arguments.") : runServerDiscoveryWorker(appDirectory, moduleNames).pipe(Effect.tap((encoded) => Effect.sync(() => writeDiscovery(encoded))))).pipe(Effect.provide(layer$1), runMain);
|
|
34996
35876
|
} else main();
|
|
34997
35877
|
//#endregion
|
|
34998
35878
|
export {};
|