Line data Source code
1 : import * as React from 'react';
2 : import { OPEN_EPHEMERAL_SESSION, useRouterNavigate } from '../app/router/router.js';
3 : import { useConnectionAllocator, useConnectionRegistryStore, sessionConnectionFromState } from '../connections/connection_registry.js';
4 : import { ConnectionState } from '../connections/connection_state.js';
5 : import { findEmbeddedConnection, newSessionConnection } from '../connections/embedded_connection.js';
6 : import { getHyperLinkTabState, isHyperLinkParams } from '../connections/hyper/hyper_link.js';
7 : import { useStorageWriter } from '../storage/storage_provider.js';
8 : import { groupSessionManifestWrites, WRITE_SESSION_MANIFEST } from '../storage/storage_writer.js';
9 : import {
10 : EPHEMERAL_SESSION_NAME,
11 : freeSessionName,
12 : isEphemeralSession,
13 : newScriptConnection,
14 : scriptConnectionOf,
15 : SessionScripts,
16 : SET_PENDING_CONNECTION,
17 : takenSessionNames,
18 : } from './scripts/session_scripts.js';
19 : import { useSessionScriptsAllocator, useSessionScriptsDispatch, useSessionScriptsRegistryReader } from './scripts/session_scripts_registry.js';
20 : import { createEmptyMetadata } from './scripts/script_types.js';
21 : import { SessionViewMode, useSessionViewMode } from './session_view_mode.js';
22 :
23 : /// Create a session whose first script runs on a connection, a session without a name gets a free one
24 : export type SessionCreation = (database: ConnectionState, name?: string) => SessionScripts;
25 :
26 : /// Allocate a session without scripts and write its first manifest
27 1 : function useSessionAllocation(): (database: ConnectionState, name: string, ephemeral: boolean) => SessionScripts {
28 1 : const allocateSessionScripts = useSessionScriptsAllocator();
29 1 : const storageWriter = useStorageWriter();
30 :
31 : // New sessions start without scripts; the feed greets them with the composer's landing prompt.
32 1 : return React.useCallback((database: ConnectionState, name: string, ephemeral: boolean) => {
33 1 : const sessionId = crypto.randomUUID();
34 1 : const [_sessionId, sessionScripts] = allocateSessionScripts({
35 1 : sessionMetadata: { ...createEmptyMetadata(), ...(ephemeral ? { ephemeral } : {}) },
36 1 : instance: database.instance,
37 1 : sessionId,
38 1 : name,
39 1 : pendingConnection: scriptConnectionOf(database),
40 1 : scripts: {},
41 1 : scriptRefs: {},
42 1 : scriptOrder: [],
43 1 : scriptFocus: { fileName: '' },
44 1 : semanticUserFocus: null,
45 1 : });
46 1 : void storageWriter.write(
47 1 : groupSessionManifestWrites(sessionId),
48 1 : { type: WRITE_SESSION_MANIFEST, value: [sessionId, [sessionConnectionFromState(database)], { ephemeral }] },
49 1 : );
50 1 : return sessionScripts;
51 1 : }, [allocateSessionScripts, storageWriter]);
52 1 : }
53 :
54 1 : export function useSessionCreation(): SessionCreation {
55 1 : const allocate = useSessionAllocation();
56 1 : const readRegistry = useSessionScriptsRegistryReader();
57 1 : return React.useCallback((database: ConnectionState, name?: string) => (
58 1 : allocate(database, name ?? freeSessionName(takenSessionNames(readRegistry().sessionScriptsMap.values())), false)
59 1 : ), [allocate, readRegistry]);
60 1 : }
61 :
62 : /// The ephemeral session of the greeter, created on the connection if there is none.
63 : ///
64 : /// There is one at most: the app creates it at launch and again after every promotion. It keeps the connection that the
65 : /// user picked, unless the connection is the one of a Hyper link: a link moves the session to its connection, and a
66 : /// session on the connection of a link that is gone moves back.
67 1 : export function useEphemeralSession(): (database: ConnectionState) => SessionScripts {
68 1 : const allocate = useSessionAllocation();
69 1 : const readRegistry = useSessionScriptsRegistryReader();
70 1 : const modifySessionScripts = useSessionScriptsDispatch();
71 1 : return React.useCallback((database: ConnectionState) => {
72 1 : const existing = [...readRegistry().sessionScriptsMap.values()].find(isEphemeralSession);
73 1 : if (existing == null) return allocate(database, EPHEMERAL_SESSION_NAME, true);
74 1 : const current = newScriptConnection(existing, null);
75 1 : if (current?.connectionId !== database.connectionId && (
76 1 : getHyperLinkTabState().current === database.connectionId || (current?.params != null && isHyperLinkParams(current.params))
77 : )) {
78 1 : void modifySessionScripts(existing.sessionId, { type: SET_PENDING_CONNECTION, value: scriptConnectionOf(database) });
79 : }
80 1 : return existing;
81 1 : }, [allocate, modifySessionScripts, readRegistry]);
82 1 : }
83 :
84 : /// The ephemeral session on the connection that new sessions start on, null before the app set up its connections
85 1 : export function useEnsureEphemeralSession(): () => SessionScripts | null {
86 1 : const registryStore = useConnectionRegistryStore();
87 1 : const allocateConnection = useConnectionAllocator();
88 1 : const ephemeralSession = useEphemeralSession();
89 1 : return React.useCallback(() => {
90 1 : const { connections } = registryStore.current;
91 1 : const embedded = findEmbeddedConnection(connections.values());
92 1 : if (embedded == null) return null;
93 1 : return ephemeralSession(newSessionConnection(connections, allocateConnection, embedded.instance));
94 1 : }, [allocateConnection, ephemeralSession, registryStore]);
95 1 : }
96 :
97 : /// Open the greeter, the feed of the ephemeral session at the root of the app
98 1 : export function useOpenGreeter(): () => void {
99 1 : const ensureEphemeralSession = useEnsureEphemeralSession();
100 1 : const navigate = useRouterNavigate();
101 1 : const { setMode } = useSessionViewMode();
102 1 : return React.useCallback(() => {
103 1 : const session = ensureEphemeralSession();
104 1 : if (session == null) return;
105 1 : setMode(SessionViewMode.Session);
106 1 : navigate({ type: OPEN_EPHEMERAL_SESSION, value: session.sessionId });
107 1 : }, [ensureEphemeralSession, navigate, setMode]);
108 1 : }
|