ignotum 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/cli/bin.mjs +1183 -447
  2. package/dist/cli/bin.mjs.map +1 -1
  3. package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
  4. package/dist/runtime/api-CAgKDij7.js.map +1 -0
  5. package/dist/runtime/{api-5XSrIeqW.d.ts → api-Cv3hMbzo.d.ts} +4 -4
  6. package/dist/runtime/client.d.ts +39 -9
  7. package/dist/runtime/client.js +362 -63
  8. package/dist/runtime/client.js.map +1 -1
  9. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js +298 -0
  10. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +1 -0
  11. package/dist/runtime/id-Bt9XWRGL.js +423 -0
  12. package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
  13. package/dist/runtime/{id-Cs82tq9Q-CK-maMgN.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +2 -2
  14. package/dist/runtime/id-E7JKqQ9r.d.ts +1 -0
  15. package/dist/runtime/index-DWU-7TIh.d.ts +185 -0
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +28 -7
  19. package/dist/runtime/internal/host.js +37 -8
  20. package/dist/runtime/internal/host.js.map +1 -1
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +2 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{pagination-D-R9NR61-CpIoHFoI.d.ts → pagination-CpYCaRYD-C2Rpp4Cw.d.ts} +42 -7
  26. package/dist/runtime/pagination-E7JKqQ9r.d.ts +1 -0
  27. package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
  28. package/dist/runtime/{schema-B9XxyRO8.js → schema-DqogEydG.js} +41 -12
  29. package/dist/runtime/schema-DqogEydG.js.map +1 -0
  30. package/dist/runtime/server.d.ts +7 -4
  31. package/dist/runtime/server.js +2 -2
  32. package/dist/runtime/server.js.map +1 -1
  33. package/dist/runtime/{sync-avN7NkcU.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
  34. package/dist/runtime/types-C1iDP2Yf-BnkfBOQf.d.ts +224 -0
  35. package/package.json +2 -2
  36. package/src/cli/agent-files.ts +10 -0
  37. package/src/cli/build/server.ts +35 -6
  38. package/src/cli/command.ts +25 -2
  39. package/src/cli/deploy.ts +34 -11
  40. package/src/client/hooks.ts +68 -0
  41. package/src/client/id.ts +259 -0
  42. package/src/client/index.ts +5 -2
  43. package/src/client/sync.ts +144 -11
  44. package/src/dev-runtime/database.ts +77 -2
  45. package/src/dev-runtime/functions.ts +111 -88
  46. package/src/dev-runtime/id.ts +190 -0
  47. package/src/dev-runtime/migrations.ts +9 -0
  48. package/src/dev-runtime/query-cache.ts +105 -0
  49. package/src/dev-runtime/sync.ts +149 -17
  50. package/src/internal/host.ts +1 -1
  51. package/src/server/index.ts +2 -0
  52. package/dist/runtime/api-DzcR7spt.js.map +0 -1
  53. package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js +0 -330
  54. package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +0 -1
  55. package/dist/runtime/file-C1abuMgd.js +0 -173
  56. package/dist/runtime/file-C1abuMgd.js.map +0 -1
  57. package/dist/runtime/index-CrWg4Z0y.d.ts +0 -398
  58. package/dist/runtime/pagination-CX5IPbEi.d.ts +0 -1
  59. package/dist/runtime/result-DIjKM-p4.d.ts +0 -1
  60. package/dist/runtime/schema-B9XxyRO8.js.map +0 -1
@@ -513,16 +513,28 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
513
513
  );
514
514
  });
515
515
 
516
- const serverFunctionEntryPlugin = (definition: DiscoveredServerFunction): Plugin => {
516
+ const serverFunctionEntryPlugin = (
517
+ definition: DiscoveredServerFunction,
518
+ schemaPath?: string,
519
+ ): Plugin => {
517
520
  const entryId = `virtual:ignotum/server-function/${definition.address}`;
518
521
  const resolvedEntryId = `\0${entryId}`;
519
- const source = `import { call } from ${encodeJavaScriptString(hostModuleId)}
522
+ const source =
523
+ schemaPath === undefined
524
+ ? `import { call } from ${encodeJavaScriptString(hostModuleId)}
520
525
  import { invoke } from "ignotum/internal/host"
521
526
  import { ${definition.exportName} as definition } from ${encodeJavaScriptString(normalizePath(definition.modulePath))}
522
527
 
523
528
  export default async function handler(encodedRequest, encodedEnvironment) {
524
529
  return invoke(definition, JSON.parse(encodedRequest), call, JSON.parse(encodedEnvironment))
525
530
  }
531
+ `
532
+ : `import { evolve } from "ignotum/internal/host"
533
+ import schema from ${encodeJavaScriptString(normalizePath(schemaPath))}
534
+
535
+ export default function handler(encodedRequest) {
536
+ return evolve(schema, encodedRequest)
537
+ }
526
538
  `;
527
539
 
528
540
  return {
@@ -537,6 +549,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
537
549
  serverOutputDirectory: string,
538
550
  conditions: ReadonlyArray<string>,
539
551
  definition: DiscoveredServerFunction,
552
+ schemaPath?: string,
540
553
  ) {
541
554
  const fileSystem = yield* FileSystem.FileSystem;
542
555
  const path = yield* Path.Path;
@@ -573,7 +586,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
573
586
  configFile: false,
574
587
  logLevel: "warn",
575
588
  mode: "production",
576
- plugins: [boundaries, serverFunctionEntryPlugin(definition)],
589
+ plugins: [boundaries, serverFunctionEntryPlugin(definition, schemaPath)],
577
590
  publicDir: false,
578
591
  resolve: {
579
592
  conditions: [...conditions],
@@ -599,8 +612,9 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
599
612
  }
600
613
  if (
601
614
  entry.dynamicImports.length > 0 ||
602
- entry.imports.length !== 1 ||
603
- entry.imports[0] !== hostModuleId ||
615
+ (schemaPath === undefined
616
+ ? entry.imports.length !== 1 || entry.imports[0] !== hostModuleId
617
+ : entry.imports.length !== 0) ||
604
618
  !entry.exports.includes("default")
605
619
  ) {
606
620
  return yield* InvalidBuildOutput.make({
@@ -667,8 +681,23 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
667
681
  (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition),
668
682
  { concurrency: 4 },
669
683
  );
684
+ const schemaPath = path.join(appDirectory, "server", "schema.ts");
685
+ const schemaEvolution = yield* buildServerFunction(
686
+ appDirectory,
687
+ outputDirectory,
688
+ conditions,
689
+ {
690
+ address: FunctionAddress.make("api._schema.evolve"),
691
+ exportName: "evolve",
692
+ kind: "Mutation",
693
+ moduleName: "_schema",
694
+ modulePath: schemaPath,
695
+ },
696
+ schemaPath,
697
+ );
670
698
  const manifest = {
671
- formatVersion: 3,
699
+ formatVersion: 4,
700
+ schemaEvolution: { bundle: schemaEvolution.bundle, sourceMap: schemaEvolution.sourceMap },
672
701
  environment: {
673
702
  artifact: yield* artifactReference(serverEnvironmentPath, environmentBytes),
674
703
  fields: discovered.environment.fields,
@@ -13,7 +13,8 @@ import {
13
13
  status as readAuthStatus,
14
14
  } from "./auth-client.js";
15
15
  import { ControlClient } from "./control-client.js";
16
- import { deploy as runDeploy } from "./deploy.js";
16
+ import { deploy as runDeploy, resumeDeployment } from "./deploy.js";
17
+ import { DeploymentId } from "@ignotum/contracts/runtime/identity";
17
18
  import { resetDevDatabase } from "../dev-runtime/dev-database.js";
18
19
  import { dev as runDev } from "./dev.js";
19
20
  import { createApp } from "./new-app.js";
@@ -144,15 +145,37 @@ const dev = Command.make(
144
145
  const deploy = Command.make(
145
146
  "deploy",
146
147
  {
148
+ resume: Flag.string("resume").pipe(
149
+ Flag.withDescription("Finish activation of an already uploaded deployment."),
150
+ Flag.withSchema(DeploymentId),
151
+ Flag.optional,
152
+ ),
147
153
  app: Flag.string("app").pipe(
148
154
  Flag.withDescription("Create or link this app slug on the first deploy."),
149
155
  Flag.withSchema(AppSlug),
150
156
  Flag.optional,
151
157
  ),
152
158
  },
153
- Effect.fn("deploy")(function* ({ app }) {
159
+ Effect.fn("deploy")(function* ({ app, resume }) {
154
160
  const path = yield* Path.Path;
155
161
  const terminal = yield* Terminal.Terminal;
162
+ if (Option.isSome(resume)) {
163
+ const result = yield* resumeDeployment(
164
+ path.resolve("."),
165
+ resume.value,
166
+ Option.getOrUndefined(app),
167
+ ).pipe(
168
+ // @effect-diagnostics-next-line strictEffectProvide:off This command owns the authenticated control client.
169
+ Effect.provide(ControlClient.layer),
170
+ Effect.mapError((error) =>
171
+ CliError.UserError.make({ cause: error, userMessage: error.message }),
172
+ ),
173
+ );
174
+ yield* terminal.display(
175
+ `Deployment activated.\n\nApp: ${result.appUrl}\nDeployment: ${result.deploymentId}\n`,
176
+ );
177
+ return;
178
+ }
156
179
  const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
157
180
  // @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
158
181
  Effect.provide(ControlClient.layer),
package/src/cli/deploy.ts CHANGED
@@ -15,7 +15,12 @@ import { appUrl, configureApp, type AppConfiguration } from "./app-configuration
15
15
  import { buildClient } from "./build/client.js";
16
16
  import { buildServer } from "./build/server.js";
17
17
  import { generate } from "./codegen.js";
18
- import { ControlClient, deploymentUploadFiles } from "./control-client.js";
18
+ import {
19
+ ControlClient,
20
+ deploymentUploadFiles,
21
+ type ControlApiError,
22
+ type ControlClientUnavailable,
23
+ } from "./control-client.js";
19
24
 
20
25
  export interface DeploymentBuildResult {
21
26
  readonly clientAssets: number;
@@ -58,6 +63,18 @@ export class DeploymentActivationUnconfirmed extends Schema.TaggedError<Deployme
58
63
  },
59
64
  ) {}
60
65
 
66
+ const activationError = (
67
+ deploymentId: DeploymentId,
68
+ cause: ControlApiError | ControlClientUnavailable,
69
+ ) =>
70
+ cause._tag === "ControlClientUnavailable" || cause.status >= 500
71
+ ? DeploymentActivationUnconfirmed.make({
72
+ cause,
73
+ deploymentId,
74
+ message: `Deployment ${deploymentId} is ready, but activation could not be confirmed. Run ignotum deploy --resume ${deploymentId} to finish activation.`,
75
+ })
76
+ : cause;
77
+
61
78
  const replaceBuildDirectory = Effect.fn("Deploy.replaceBuildDirectory")(function* (
62
79
  stateDirectory: string,
63
80
  stagingDirectory: string,
@@ -192,16 +209,9 @@ export const uploadDeployment = Effect.fn("Deploy.upload")(function* (
192
209
  }),
193
210
  );
194
211
 
195
- yield* control.activateDeployment(configuration.appId, deployment.id).pipe(
196
- Effect.catchTags({
197
- ControlClientUnavailable: (cause) =>
198
- DeploymentActivationUnconfirmed.make({
199
- cause,
200
- deploymentId: deployment.id,
201
- message: `Deployment ${deployment.id} is ready, but activation could not be confirmed.`,
202
- }),
203
- }),
204
- );
212
+ yield* control
213
+ .activateDeployment(configuration.appId, deployment.id)
214
+ .pipe(Effect.mapError((cause) => activationError(deployment.id, cause)));
205
215
 
206
216
  return deployment.id;
207
217
  });
@@ -220,3 +230,16 @@ export const deploy = Effect.fn("Deploy.run")(function* (
220
230
  deploymentId,
221
231
  } satisfies DeployResult;
222
232
  });
233
+
234
+ export const resumeDeployment = Effect.fn("Deploy.resume")(function* (
235
+ appDirectory: string,
236
+ deploymentId: DeploymentId,
237
+ requestedSlug?: AppSlug,
238
+ ) {
239
+ const configuration = yield* configureApp(appDirectory, requestedSlug);
240
+ const control = yield* ControlClient;
241
+ yield* control
242
+ .activateDeployment(configuration.appId, deploymentId)
243
+ .pipe(Effect.mapError((cause) => activationError(deploymentId, cause)));
244
+ return { appUrl: appUrl(configuration.slug), deploymentId };
245
+ });
@@ -1,3 +1,5 @@
1
+ import { BrowserSession, type IdState } from "./id.js";
2
+ import type { ProfileField } from "@ignotum/contracts/id";
1
3
  import { Effect, Schema } from "effect";
2
4
  import { useCallback, useDebugValue, useEffect, useMemo, useState } from "preact/hooks";
3
5
 
@@ -297,3 +299,69 @@ export const useMutation = <Args extends object | void, Success, Failure extends
297
299
  // both call shapes normalize omitted arguments to the empty JSON object.
298
300
  return mutate as Mutation<Args, Success, Failure>;
299
301
  }, [reference]);
302
+
303
+ export type IdOptions = {
304
+ readonly profile?: ReadonlyArray<ProfileField>;
305
+ readonly returnTo?: string;
306
+ };
307
+ export type IdResult = IdState & {
308
+ readonly signIn: (options?: IdOptions) => Promise<void>;
309
+ readonly requestProfile: (
310
+ fields: ReadonlyArray<ProfileField>,
311
+ options?: { readonly returnTo?: string },
312
+ ) => Promise<void>;
313
+ readonly signOut: () => Promise<void>;
314
+ };
315
+
316
+ export function useId(): IdResult {
317
+ const [state, setState] = useState<IdState>({ status: "pending" });
318
+ useEffect(() => {
319
+ let active = true;
320
+ let unsubscribe: (() => void) | undefined;
321
+ void syncRuntime.runPromise(BrowserSession).then((session) => {
322
+ if (!active) return;
323
+ const update = () => setState(session.snapshot());
324
+ unsubscribe = session.subscribe(update);
325
+ update();
326
+ });
327
+ return () => {
328
+ active = false;
329
+ unsubscribe?.();
330
+ };
331
+ }, []);
332
+ const actions = useMemo(
333
+ () => ({
334
+ signIn: (options?: IdOptions) =>
335
+ syncRuntime.runPromise(
336
+ Effect.gen(function* () {
337
+ return yield* (yield* BrowserSession).start(
338
+ "signIn",
339
+ options?.profile ?? [],
340
+ options?.returnTo,
341
+ );
342
+ }),
343
+ ),
344
+ requestProfile: (
345
+ fields: ReadonlyArray<ProfileField>,
346
+ options?: { readonly returnTo?: string },
347
+ ) =>
348
+ syncRuntime.runPromise(
349
+ Effect.gen(function* () {
350
+ return yield* (yield* BrowserSession).start(
351
+ "requestProfile",
352
+ fields,
353
+ options?.returnTo,
354
+ );
355
+ }),
356
+ ),
357
+ signOut: () =>
358
+ syncRuntime.runPromise(
359
+ Effect.gen(function* () {
360
+ return yield* (yield* BrowserSession).signOut;
361
+ }),
362
+ ),
363
+ }),
364
+ [],
365
+ );
366
+ return { ...state, ...actions };
367
+ }
@@ -0,0 +1,259 @@
1
+ import { Context, Effect, Layer, Option, Schema, Semaphore } from "effect";
2
+ import {
3
+ FetchHttpClient,
4
+ HttpClient,
5
+ HttpClientRequest,
6
+ HttpClientResponse,
7
+ } from "effect/unstable/http";
8
+ import {
9
+ DevelopmentSelection,
10
+ developmentSelectionKey,
11
+ SessionOutcome,
12
+ SessionRedirect,
13
+ SessionRequest,
14
+ SessionResponse,
15
+ sessionPath,
16
+ sessionRequestsPath,
17
+ type ProfileField,
18
+ type SessionState,
19
+ type User,
20
+ } from "@ignotum/contracts/id";
21
+ import { IdGenerator } from "@ignotum/shared/id";
22
+ import { SessionEpoch } from "@ignotum/contracts/runtime/id";
23
+
24
+ export class IdRequestError extends Schema.TaggedError<IdRequestError>()("IdRequestError", {
25
+ message: Schema.String,
26
+ }) {}
27
+
28
+ export type IdState =
29
+ | { readonly status: "pending"; readonly outcome?: SessionOutcome }
30
+ | { readonly status: "error"; readonly error: string; readonly outcome?: SessionOutcome }
31
+ | { readonly status: "signedOut"; readonly outcome?: SessionOutcome }
32
+ | { readonly status: "signedIn"; readonly user: User; readonly outcome?: SessionOutcome };
33
+
34
+ const outcomeKey = "ignotum.id.outcome";
35
+ const selectionJson = Schema.fromJsonString(DevelopmentSelection);
36
+
37
+ export class BrowserSession extends Context.Service<
38
+ BrowserSession,
39
+ {
40
+ readonly snapshot: () => IdState;
41
+ readonly current: () => SessionState | undefined;
42
+ readonly subscribe: (listener: () => void) => () => void;
43
+ readonly bootstrap: Effect.Effect<SessionState, IdRequestError>;
44
+ readonly accept: (session: SessionState, serverTime: number) => void;
45
+ readonly suspend: (error?: string) => void;
46
+ readonly socketParameters: () => string;
47
+ readonly start: (
48
+ intent: "signIn" | "requestProfile",
49
+ profile: ReadonlyArray<ProfileField>,
50
+ returnTo?: string,
51
+ ) => Effect.Effect<void, IdRequestError>;
52
+ readonly signOut: Effect.Effect<void, IdRequestError>;
53
+ }
54
+ >()("ignotum/client/id/BrowserSession") {
55
+ static readonly layer = Layer.effect(
56
+ BrowserSession,
57
+ Effect.gen(function* () {
58
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
59
+ const ids = yield* IdGenerator;
60
+ const bootstrapLock = yield* Semaphore.make(1);
61
+ const listeners = new Set<() => void>();
62
+ let state: IdState = { status: "pending" };
63
+ let current: SessionState | undefined;
64
+ let generation = 0;
65
+ let deadline = Infinity;
66
+ let development = false;
67
+ let selection: DevelopmentSelection | undefined;
68
+ let expiry: ReturnType<typeof setTimeout> | undefined;
69
+ const notify = () => {
70
+ for (const listener of listeners) listener();
71
+ };
72
+ const suspend = (error?: string) => {
73
+ generation += 1;
74
+ deadline = Infinity;
75
+ if (expiry !== undefined) clearTimeout(expiry);
76
+ current = undefined;
77
+ state =
78
+ error === undefined
79
+ ? { status: "pending", outcome: state.outcome }
80
+ : { status: "error", error, outcome: state.outcome };
81
+ notify();
82
+ };
83
+ const accept = (session: SessionState, serverTime: number) => {
84
+ generation += 1;
85
+ if (expiry !== undefined) clearTimeout(expiry);
86
+ current = session;
87
+ state =
88
+ session.user === null
89
+ ? { status: "signedOut", outcome: state.outcome }
90
+ : { status: "signedIn", user: session.user, outcome: state.outcome };
91
+ const delay = Math.max(0, session.validUntil - serverTime);
92
+ deadline = performance.now() + delay;
93
+ if (delay < 2_147_483_647) expiry = setTimeout(() => suspend(), delay);
94
+ notify();
95
+ };
96
+ const checkExpiry = () => {
97
+ if (current !== undefined && performance.now() >= deadline) suspend();
98
+ };
99
+ globalThis.addEventListener?.("focus", checkExpiry);
100
+ globalThis.document?.addEventListener("visibilitychange", checkExpiry);
101
+ yield* Effect.addFinalizer(() =>
102
+ Effect.sync(() => {
103
+ if (expiry !== undefined) clearTimeout(expiry);
104
+ listeners.clear();
105
+ globalThis.removeEventListener?.("focus", checkExpiry);
106
+ globalThis.document?.removeEventListener("visibilitychange", checkExpiry);
107
+ }),
108
+ );
109
+
110
+ const readOutcome = () => {
111
+ const raw = globalThis.sessionStorage.getItem(outcomeKey);
112
+ if (raw !== null) {
113
+ const decoded = Schema.decodeUnknownOption(SessionOutcome)(raw);
114
+ if (Option.isSome(decoded)) state = { ...state, outcome: decoded.value };
115
+ globalThis.sessionStorage.removeItem(outcomeKey);
116
+ }
117
+ };
118
+ const parameters = () => {
119
+ if (!development || selection === undefined) return "";
120
+ const params = new URLSearchParams({ epoch: selection.epoch });
121
+ if (selection.username !== null) params.set("username", selection.username);
122
+ return `?${params}`;
123
+ };
124
+ const readSession = Effect.fn("BrowserSession.read")(function* () {
125
+ return yield* client
126
+ .get(`${sessionPath}${parameters()}`, { headers: { "x-ignotum-request": "1" } })
127
+ .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionResponse)));
128
+ });
129
+ let bootstrapGeneration = 0;
130
+ const bootstrap = bootstrapLock
131
+ .withPermits(1)(
132
+ Effect.gen(function* () {
133
+ const admittedGeneration = generation;
134
+ bootstrapGeneration = admittedGeneration;
135
+ const startedAt = performance.now();
136
+ let session = yield* readSession();
137
+ development = session.development === true;
138
+ if (development && selection === undefined) {
139
+ const saved = globalThis.sessionStorage.getItem(developmentSelectionKey);
140
+ const decoded =
141
+ saved === null
142
+ ? Option.none<DevelopmentSelection>()
143
+ : Schema.decodeOption(selectionJson)(saved);
144
+ selection = Option.getOrElse(decoded, () => ({
145
+ formatVersion: 1 as const,
146
+ username: null,
147
+ epoch: "",
148
+ }));
149
+ if (selection.epoch === "")
150
+ selection = { ...selection, epoch: yield* ids.generate(SessionEpoch) };
151
+ globalThis.sessionStorage.setItem(
152
+ developmentSelectionKey,
153
+ Schema.encodeSync(selectionJson)(selection),
154
+ );
155
+ session = yield* readSession();
156
+ }
157
+ if (generation !== admittedGeneration) {
158
+ if (current !== undefined) return current;
159
+ return yield* IdRequestError.make({
160
+ message: "Your ID session changed while loading.",
161
+ });
162
+ }
163
+ readOutcome();
164
+ if (session.outcome !== undefined) state = { ...state, outcome: session.outcome };
165
+ accept(session, session.serverTime + performance.now() - startedAt);
166
+ return session;
167
+ }),
168
+ )
169
+ .pipe(
170
+ Effect.mapError(() =>
171
+ IdRequestError.make({ message: "Could not load your ID session." }),
172
+ ),
173
+ Effect.tapError((error) =>
174
+ Effect.sync(() => {
175
+ if (generation === bootstrapGeneration) suspend(error.message);
176
+ }),
177
+ ),
178
+ );
179
+
180
+ const start = Effect.fn("BrowserSession.start")(function* (
181
+ intent: "signIn" | "requestProfile",
182
+ profile: ReadonlyArray<ProfileField>,
183
+ returnTo?: string,
184
+ ) {
185
+ const session = current ?? (yield* bootstrap);
186
+ if (intent === "requestProfile" && session.user === null)
187
+ return yield* IdRequestError.make({
188
+ message: "Sign in before requesting profile information.",
189
+ });
190
+ const request = yield* Schema.decodeUnknownEffect(SessionRequest)({
191
+ intent,
192
+ profile,
193
+ sessionEpoch: session.sessionEpoch,
194
+ returnTo:
195
+ returnTo ??
196
+ `${globalThis.location.pathname}${globalThis.location.search}${globalThis.location.hash}`,
197
+ }).pipe(
198
+ Effect.mapError(() => IdRequestError.make({ message: "The ID request is invalid." })),
199
+ );
200
+ const redirect = yield* HttpClientRequest.post(sessionRequestsPath).pipe(
201
+ HttpClientRequest.setHeader("x-ignotum-request", "1"),
202
+ HttpClientRequest.bodyJsonUnsafe(request),
203
+ client.execute,
204
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionRedirect)),
205
+ Effect.mapError(() =>
206
+ IdRequestError.make({ message: "Could not start the ID request." }),
207
+ ),
208
+ );
209
+ if (current?.sessionEpoch !== session.sessionEpoch)
210
+ return yield* IdRequestError.make({ message: "Your ID session changed. Try again." });
211
+ globalThis.location.assign(redirect.redirectUrl);
212
+ });
213
+ const signOut = Effect.gen(function* () {
214
+ const session = current ?? (yield* bootstrap);
215
+ yield* HttpClientRequest.delete(sessionPath).pipe(
216
+ HttpClientRequest.setHeader("x-ignotum-request", "1"),
217
+ HttpClientRequest.bodyJsonUnsafe({ sessionEpoch: session.sessionEpoch }),
218
+ client.execute,
219
+ Effect.mapError(() => IdRequestError.make({ message: "Could not sign out." })),
220
+ );
221
+ suspend();
222
+ if (development) {
223
+ selection = {
224
+ formatVersion: 1,
225
+ username: null,
226
+ epoch: yield* ids.generate(SessionEpoch),
227
+ };
228
+ globalThis.sessionStorage.setItem(
229
+ developmentSelectionKey,
230
+ Schema.encodeSync(selectionJson)(selection),
231
+ );
232
+ }
233
+ globalThis.location.reload();
234
+ });
235
+ return BrowserSession.of({
236
+ snapshot: () => {
237
+ checkExpiry();
238
+ return state;
239
+ },
240
+ current: () => {
241
+ checkExpiry();
242
+ return current;
243
+ },
244
+ subscribe: (listener) => {
245
+ listeners.add(listener);
246
+ return () => {
247
+ listeners.delete(listener);
248
+ };
249
+ },
250
+ accept,
251
+ suspend,
252
+ bootstrap,
253
+ start,
254
+ signOut,
255
+ socketParameters: parameters,
256
+ });
257
+ }),
258
+ ).pipe(Layer.provide(FetchHttpClient.layer));
259
+ }
@@ -16,7 +16,7 @@ export type {
16
16
  FileValue,
17
17
  } from "@ignotum/contracts/schema/file";
18
18
  export { Files } from "./files.js";
19
- export { useMutation, usePaginatedQuery, useQuery } from "./hooks.js";
19
+ export { useId, useMutation, usePaginatedQuery, useQuery } from "./hooks.js";
20
20
  export type { PaginatedQueryOptions, PaginatedQueryValue, PaginationStatus } from "./hooks.js";
21
21
  export { Query } from "./query.js";
22
22
  export type { QuerySkip } from "./query.js";
@@ -89,7 +89,7 @@ export {
89
89
  useDebugValue,
90
90
  useEffect,
91
91
  useErrorBoundary,
92
- useId,
92
+ useId as useElementId,
93
93
  useImperativeHandle,
94
94
  useLayoutEffect,
95
95
  useMemo,
@@ -98,3 +98,6 @@ export {
98
98
  useState,
99
99
  } from "preact/hooks";
100
100
  export type { Dispatch, Reducer, StateUpdater } from "preact/hooks";
101
+
102
+ export type { User, UserId, ProfileField } from "@ignotum/contracts/id";
103
+ export type { IdResult, IdOptions } from "./hooks.js";