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.
- package/dist/cli/bin.mjs +1183 -447
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
- package/dist/runtime/api-CAgKDij7.js.map +1 -0
- package/dist/runtime/{api-5XSrIeqW.d.ts → api-Cv3hMbzo.d.ts} +4 -4
- package/dist/runtime/client.d.ts +39 -9
- package/dist/runtime/client.js +362 -63
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js +298 -0
- package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +1 -0
- package/dist/runtime/id-Bt9XWRGL.js +423 -0
- package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-CK-maMgN.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +2 -2
- package/dist/runtime/id-E7JKqQ9r.d.ts +1 -0
- package/dist/runtime/index-DWU-7TIh.d.ts +185 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +28 -7
- package/dist/runtime/internal/host.js +37 -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 +2 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-D-R9NR61-CpIoHFoI.d.ts → pagination-CpYCaRYD-C2Rpp4Cw.d.ts} +42 -7
- package/dist/runtime/pagination-E7JKqQ9r.d.ts +1 -0
- package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
- package/dist/runtime/{schema-B9XxyRO8.js → schema-DqogEydG.js} +41 -12
- package/dist/runtime/schema-DqogEydG.js.map +1 -0
- package/dist/runtime/server.d.ts +7 -4
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-avN7NkcU.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
- package/dist/runtime/types-C1iDP2Yf-BnkfBOQf.d.ts +224 -0
- package/package.json +2 -2
- package/src/cli/agent-files.ts +10 -0
- package/src/cli/build/server.ts +35 -6
- package/src/cli/command.ts +25 -2
- package/src/cli/deploy.ts +34 -11
- package/src/client/hooks.ts +68 -0
- package/src/client/id.ts +259 -0
- package/src/client/index.ts +5 -2
- package/src/client/sync.ts +144 -11
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +111 -88
- package/src/dev-runtime/id.ts +190 -0
- package/src/dev-runtime/migrations.ts +9 -0
- package/src/dev-runtime/query-cache.ts +105 -0
- package/src/dev-runtime/sync.ts +149 -17
- package/src/internal/host.ts +1 -1
- package/src/server/index.ts +2 -0
- package/dist/runtime/api-DzcR7spt.js.map +0 -1
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js +0 -330
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +0 -1
- package/dist/runtime/file-C1abuMgd.js +0 -173
- package/dist/runtime/file-C1abuMgd.js.map +0 -1
- package/dist/runtime/index-CrWg4Z0y.d.ts +0 -398
- package/dist/runtime/pagination-CX5IPbEi.d.ts +0 -1
- package/dist/runtime/result-DIjKM-p4.d.ts +0 -1
- package/dist/runtime/schema-B9XxyRO8.js.map +0 -1
package/src/cli/build/server.ts
CHANGED
|
@@ -513,16 +513,28 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
513
513
|
);
|
|
514
514
|
});
|
|
515
515
|
|
|
516
|
-
const serverFunctionEntryPlugin = (
|
|
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 =
|
|
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
|
-
|
|
603
|
-
|
|
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:
|
|
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,
|
package/src/cli/command.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
});
|
package/src/client/hooks.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/client/id.ts
ADDED
|
@@ -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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -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";
|