oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "context"
5 "log/slog"
6 "time"
7
8 "amadan.net/oficina/home/connect"
9 "amadan.net/oficina/home/handoff/seal"
10 "amadan.net/oficina/slides/internal/slides/core"
11 "amadan.net/oficina/slides/internal/slides/live"
12 "amadan.net/oficina/slides/internal/slides/present"
13 rauth "amadan.net/rastrillo/rastrillo/auth"
14 "amadan.net/rastrillo/rastrillo/db"
15 "amadan.net/rastrillo/rastrillo/jobs"
16 "amadan.net/rastrillo/rastrillo/sessions"
17)
18
19// app holds what every handler needs: the database, the logger, the
20// RESOLVED config — every fail-closed default already applied, so
21// a.cfg.Origin is the origin this app is really protecting and
22// a.cfg.Mailer is the sender it is really using — the parsed allowlist,
23// and the deck core. Config is also where the object store arrives
24// (Config.Blobs) for the image screens that come next.
25//
26// The handlers themselves live beside what they serve: sign-in in
27// auth.go, the deck screens in decks.go.
28type app struct {
29 home *connect.Bridge
30 homeRoutes *connect.Routes
31 homeSessions *sessions.Sessions
32 // sessions reads the session cookie on a page outside the signed-in
33 // group (an error page, Open source, the share-link viewer), so the
34 // bar shows who is signed in there too. The same table and cookie
35 // rauth's own store uses; never used to sign anybody in.
36 sessions *sessions.Sessions
37 // directory is the hub directory (HOME_DIRECTORY_ORIGIN), set only
38 // with Home configured: the app switcher's "All apps" link. Empty,
39 // no switcher.
40 directory string
41 db *db.DB
42 logger *slog.Logger
43 cfg Config
44 allowed allowlist
45 // mail is the sign-in mail queue rauth sends through (mailqueue.go).
46 mail *mailQueue
47 // returnSeal authenticates the short-lived return_to cookie. It is
48 // derived from this instance's sign-in key, never from a browser
49 // value, and the clock is a seam for expiry tests.
50 returnSeal *seal.Sealer
51 returnNow func() time.Time
52 // core is the compiled deck core, shared by every request.
53 // Compiling the wasm module is expensive and instantiating it is
54 // not, so there is exactly one of these per process and a Handle
55 // (one module instance) per deck opened.
56 core *core.Runtime
57 // auth is the sign-in plugin, held for one thing the router
58 // cannot do: resolving a session cookie on a screen mounted
59 // OUTSIDE the signed-in group. RequireSession is what puts a
60 // session on the request context, so past it sessions.Current
61 // answers nothing — and the presenting screens (presentation.go)
62 // are all capability-addressed, reachable with no session at all,
63 // while still needing to know whether the person reading is the
64 // presenter. rauth's SessionFrom is that direct lookup.
65 auth *rauth.Auth
66 // hub is the in-process fan-out behind every live presentation:
67 // one command, every watching page told (internal/slides/present).
68 // One per app, because a subscriber list nobody else can reach is
69 // not a fan-out.
70 hub *present.Hub
71 // jobs is rastrillo's background-work registry and jobPages its
72 // three handlers (status page, polled fragment, SSE). One per app:
73 // a job is a goroutine in this process and the registry is how a
74 // person watches it (exports.go).
75 jobs *jobs.Jobs
76 jobPages *jobs.Handlers
77 // exports is what this app holds beside those jobs: the finished
78 // PDF, or the object-store key it was uploaded under, per job
79 // (exports.go). rastrillo/jobs carries a status and a sentence and
80 // deliberately nothing of the work's own result.
81 exports *exportStore
82 // pdf is the compiled PDF module, built on the first export and
83 // closed with the app. See pdfRuntime for why it is lazy where the
84 // deck core is not.
85 pdf *pdfRuntime
86 // live is the in-process fan-out behind every deck being EDITED:
87 // one accepted update announced as a seq, plus who is currently in
88 // the deck (internal/slides/live). Separate from hub above because
89 // they are separate subjects with separate lifetimes — a talk being
90 // given and a document being written — and folding them into one
91 // map keyed by two kinds of sqid would make every publish a
92 // question about which kind it was.
93 live *live.Hub
94}
95
96// Close releases the compiled deck core's wazero runtime — the one
97// resource New builds for itself rather than taking from its caller
98// (newApp's doc comment: the database handle is the caller's, closed
99// there). It does not close the database: New never opened it, so it
100// is never this method's to release.
101//
102// New hands back the *app this closes alongside the mux it also
103// returns; a caller that never stops (a long-running server whose
104// process exit reclaims everything anyway) is free to ignore it, but
105// cmd/slides/main.go's shutdown path and every test harness call it so
106// a compiled module is not leaked per app built.
107func (a *app) Close() error {
108 if a == nil {
109 return nil
110 }
111 // The live hub's sweeper is a goroutine, so it is stopped here for
112 // the same reason the runtime is closed here: a test binary builds
113 // one app per test, and a resource nobody releases is a leak per
114 // app rather than per process.
115 if a.live != nil {
116 a.live.Close()
117 }
118 // Queued sign-in links are sent before the process lets go.
119 if a.mail != nil {
120 a.mail.close()
121 }
122 // The PDF module, if any export ever built one. Same argument as
123 // the deck core's below: a test binary builds an app per test, and
124 // a compiled wasm module nobody releases is a leak per app.
125 if err := a.pdf.close(context.Background()); err != nil {
126 a.logger.Error("slides: close the pdf runtime", "err", err)
127 }
128 if a.core == nil {
129 return nil
130 }
131 return a.core.Close(context.Background())
132}
133
134// FlushMail waits until every sign-in link queued so far has been sent
135// or dropped. The links go out after the response (mailqueue.go), so a
136// test reads the mailer only after this.
137func (a *app) FlushMail() {
138 if a.mail != nil {
139 a.mail.wait()
140 }
141}
142