pi-roundtable-web 0.25.0 → 0.26.1
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/CHANGELOG.md +54 -0
- package/README.md +165 -12
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-ghq105t4.js +1 -0
- package/dist/chunk-action-menu-me0g9qsc.js +1 -0
- package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
- package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
- package/dist/chunk-character-creator-bd426yjw.js +1 -0
- package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
- package/dist/chunk-confirm-p3mts8mx.js +1 -0
- package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
- package/dist/chunk-conversations-0e0vbxwr.js +1 -0
- package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
- package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
- package/dist/chunk-folder-list-era3tksh.js +1 -0
- package/dist/chunk-folds-hvws0bd5.js +1 -0
- package/dist/chunk-gate-hc7dd0fx.js +1 -0
- package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
- package/dist/chunk-group-table-fqmxgdfw.js +1 -0
- package/dist/chunk-host-icons-4xde9ntk.js +1 -0
- package/dist/chunk-host-picture-014yzk20.js +1 -0
- package/dist/chunk-host-state-t56e5sdy.js +1 -0
- package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
- package/dist/chunk-index-0fmmvgee.js +2 -0
- package/dist/chunk-index-0q15ysrb.js +1 -0
- package/dist/chunk-index-0xh24y25.js +2 -0
- package/dist/chunk-index-2yebd9tb.js +426 -0
- package/dist/chunk-index-4t0r3t3b.js +2 -0
- package/dist/chunk-index-5yzfrf7b.js +138 -0
- package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
- package/dist/chunk-index-73gh7kgr.js +920 -0
- package/dist/chunk-index-7xffskfa.js +8 -0
- package/dist/chunk-index-aqw7cy5j.js +1 -0
- package/dist/chunk-index-b6ca02e8.js +2 -0
- package/dist/chunk-index-b8nvhcx5.js +2 -0
- package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
- package/dist/chunk-index-czdketze.js +2 -0
- package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
- package/dist/chunk-index-eg55mqqe.js +1 -0
- package/dist/chunk-index-gr6e0hse.js +2 -0
- package/dist/chunk-index-gyary7np.js +13 -0
- package/dist/chunk-index-j214tczc.js +1 -0
- package/dist/chunk-index-jd6632ef.js +2 -0
- package/dist/chunk-index-jw7dtshr.js +2 -0
- package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
- package/dist/chunk-index-kwcx62wb.js +4 -0
- package/dist/chunk-index-m7fvw92f.js +2 -0
- package/dist/chunk-index-mmg788a1.js +2 -0
- package/dist/chunk-index-mq0dacwr.js +2 -0
- package/dist/chunk-index-p53ak25v.js +2 -0
- package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
- package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
- package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
- package/dist/chunk-index-rnxj3z8k.js +2 -0
- package/dist/chunk-index-snzzfhk3.js +2 -0
- package/dist/chunk-index-v3bmr0nw.js +2 -0
- package/dist/chunk-index-vcyzwtk2.js +2 -0
- package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
- package/dist/chunk-index-w31c4yjv.js +2 -0
- package/dist/chunk-index-wrm8w165.js +2 -0
- package/dist/chunk-index-xh0mg9cw.js +2 -0
- package/dist/chunk-index-ybtvvx85.js +2 -0
- package/dist/chunk-index-yzsgsvnp.js +2 -0
- package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
- package/dist/chunk-index-zrvg23jd.js +2 -0
- package/dist/chunk-index-zynq0b0w.js +50 -0
- package/dist/chunk-lang-css-88v3hp3d.js +1 -0
- package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
- package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
- package/dist/chunk-lang-html-07d47es4.js +1 -0
- package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
- package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
- package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
- package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
- package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
- package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
- package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
- package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
- package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
- package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
- package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
- package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
- package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
- package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
- package/dist/chunk-note-row-kd0fy7ee.js +75 -0
- package/dist/chunk-notes-yb8f057c.js +1 -0
- package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
- package/dist/chunk-notices-x07da5bb.js +1 -0
- package/dist/chunk-onboarding-csrkbh9z.js +1 -0
- package/dist/chunk-overview-avc2d30t.js +1 -0
- package/dist/chunk-palette-9nhp95bz.js +1 -0
- package/dist/chunk-placement-items-q694dpgk.js +1 -0
- package/dist/chunk-profile-6qemf3m7.js +1 -0
- package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
- package/dist/chunk-prompts-nv6mrn0q.js +1 -0
- package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
- package/dist/chunk-room-menu-rr6nzap1.js +1 -0
- package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
- package/dist/chunk-schedules-rw57nr0m.js +1 -0
- package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
- package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
- package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
- package/dist/chunk-skills-tnr1h16h.js +1 -0
- package/dist/chunk-switcher-list-17enhrse.js +1 -0
- package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
- package/dist/chunk-table-r12cja6p.js +1 -0
- package/dist/chunk-table-seats-g3t392zz.js +1 -0
- package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
- package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
- package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
- package/dist/chunk-transcript-n2s6dher.js +1 -0
- package/dist/chunk-where-f3scdydc.js +1 -0
- package/dist/chunk-workspace-xs023bxm.js +1 -0
- package/dist/chunk-zh-tw-efvmk3fd.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +5 -4
- package/src/api-types.ts +356 -4
- package/src/app/app.ts +201 -32
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/presence.ts +2 -2
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +153 -14
- package/src/app/rooms.ts +39 -0
- package/src/app/routes.ts +168 -28
- package/src/app/socket.ts +349 -81
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/threads.ts +31 -2
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +77 -8
- package/src/auth/login-page.ts +4 -2
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +62 -0
- package/src/auth/oidc.ts +649 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +173 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +102 -12
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +112 -0
- package/src/host/context.ts +188 -0
- package/src/host/forms.ts +240 -0
- package/src/host/frames.ts +349 -0
- package/src/host/host-app.ts +178 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +396 -0
- package/src/host/pages.ts +266 -0
- package/src/host/reads.ts +155 -0
- package/src/host/retention.ts +41 -0
- package/src/host/room-views.ts +137 -0
- package/src/host/rooms-service.ts +429 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +106 -0
- package/src/i18n/zh-tw.ts +139 -0
- package/src/index.ts +15 -1
- package/src/notices/api.ts +43 -0
- package/src/notices/mount.ts +292 -0
- package/src/notices/service.ts +294 -0
- package/src/notices/store.ts +298 -0
- package/src/notices/urls.ts +38 -0
- package/src/onboarding/api.ts +32 -0
- package/src/onboarding/service.ts +149 -0
- package/src/onboarding/steps.ts +75 -0
- package/src/onboarding/waivers.ts +40 -0
- package/src/options.ts +534 -34
- package/src/page-presentation.ts +47 -2
- package/src/profile/api.ts +175 -0
- package/src/profile/links.ts +162 -0
- package/src/push/api.ts +189 -52
- package/src/push/dispatch.ts +156 -0
- package/src/push/drain.ts +139 -0
- package/src/push/outbox.ts +218 -0
- package/src/push/prefs.ts +118 -0
- package/src/push/sender.ts +7 -2
- package/src/push/triggers.ts +56 -5
- package/src/push/vapid.ts +19 -0
- package/src/push/web-push-transport.ts +106 -0
- package/src/push-sender/index.ts +14 -0
- package/src/push-sender/run.ts +118 -0
- package/src/push-sender/session.ts +54 -0
- package/src/pwa.ts +32 -4
- package/src/visitors.ts +94 -6
- package/src/web-app.ts +718 -0
- package/src/web-plugin.ts +279 -26
- package/dist/chunk-action-menu-qe839g4b.js +0 -1
- package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
- package/dist/chunk-character-creator-6yf29xwx.js +0 -1
- package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
- package/dist/chunk-confirm-xrw7d732.js +0 -1
- package/dist/chunk-conversations-ry228aes.js +0 -1
- package/dist/chunk-folder-list-5cp8stna.js +0 -1
- package/dist/chunk-folds-hff104tk.js +0 -1
- package/dist/chunk-group-scene-h228jdat.js +0 -1
- package/dist/chunk-index-0p5h2kdz.js +0 -2
- package/dist/chunk-index-0pt08yd9.js +0 -1
- package/dist/chunk-index-12bw9yj8.js +0 -2
- package/dist/chunk-index-2ad17eh4.js +0 -2
- package/dist/chunk-index-31wh5twj.js +0 -52
- package/dist/chunk-index-4dy94pbz.js +0 -2
- package/dist/chunk-index-4ecjtdy8.js +0 -13
- package/dist/chunk-index-4v1b7ks9.js +0 -2
- package/dist/chunk-index-5d8yjj4m.js +0 -2
- package/dist/chunk-index-89ajmxh7.js +0 -2
- package/dist/chunk-index-b9ny4623.js +0 -2
- package/dist/chunk-index-e1dr6g10.js +0 -2
- package/dist/chunk-index-gpkzhq1g.js +0 -2
- package/dist/chunk-index-hk3xyy20.js +0 -2
- package/dist/chunk-index-jm23vn03.js +0 -4
- package/dist/chunk-index-k7gh0jvz.js +0 -2
- package/dist/chunk-index-ky42ev0b.js +0 -2
- package/dist/chunk-index-pa2yyqh0.js +0 -2
- package/dist/chunk-index-pcvt668q.js +0 -2
- package/dist/chunk-index-pqw27eye.js +0 -1
- package/dist/chunk-index-wmbmmrd4.js +0 -302
- package/dist/chunk-index-ymbjrsp6.js +0 -2
- package/dist/chunk-lang-css-t85r8rz0.js +0 -1
- package/dist/chunk-lang-html-n6jparge.js +0 -1
- package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
- package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
- package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
- package/dist/chunk-layout-actions-bv0chj41.js +0 -1
- package/dist/chunk-notes-9b7pgqfe.js +0 -1
- package/dist/chunk-overview-8yw71esz.js +0 -1
- package/dist/chunk-palette-frms7axe.js +0 -1
- package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
- package/dist/chunk-prompt-history-yrqehads.js +0 -1
- package/dist/chunk-prompts-g6609tqx.js +0 -1
- package/dist/chunk-room-confirms-0a2y0925.js +0 -1
- package/dist/chunk-room-menu-g11q97br.js +0 -1
- package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
- package/dist/chunk-schedules-ezmacrbj.js +0 -1
- package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
- package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
- package/dist/chunk-skills-gqcfhq56.js +0 -1
- package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
- package/dist/chunk-table-s8cde1xz.js +0 -1
- package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
- package/dist/chunk-transcript-c59c6mpm.js +0 -1
- package/dist/chunk-workspace-0cyspkk4.js +0 -1
- package/dist/chunk-zh-tw-nc93jm10.js +0 -1
package/src/app/app.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ConversationModes,
|
|
7
7
|
ConversationPort,
|
|
8
8
|
DirectChannelProvider,
|
|
9
|
+
EventSink,
|
|
9
10
|
IdentityService,
|
|
10
11
|
Logger,
|
|
11
12
|
Migration,
|
|
@@ -13,9 +14,21 @@ import type {
|
|
|
13
14
|
WebSocketRoute,
|
|
14
15
|
} from "pi-roundtable";
|
|
15
16
|
import { channelKey } from "pi-roundtable";
|
|
17
|
+
import { APP_CLOSE_EXPIRED } from "../api-types.ts";
|
|
16
18
|
import type { Authenticated, Authenticator } from "../auth/authenticator.ts";
|
|
17
19
|
import type { ConsolePresentation } from "../features.ts";
|
|
18
|
-
import {
|
|
20
|
+
import type { Gate } from "../gate.ts";
|
|
21
|
+
import { HostCardLog } from "../host/cards.ts";
|
|
22
|
+
import { HostApp } from "../host/host-app.ts";
|
|
23
|
+
import { RoomReads } from "../host/reads.ts";
|
|
24
|
+
import {
|
|
25
|
+
mountNotices,
|
|
26
|
+
NOTICE_MIGRATIONS,
|
|
27
|
+
type NoticesMount,
|
|
28
|
+
type NoticesMountOptions,
|
|
29
|
+
} from "../notices/mount.ts";
|
|
30
|
+
import type { ResolvedOptions } from "../options.ts";
|
|
31
|
+
import type { PushApi } from "../push/api.ts";
|
|
19
32
|
import { webPushChannel } from "../push/channel.ts";
|
|
20
33
|
import { EndpointPolicy } from "../push/endpoint.ts";
|
|
21
34
|
import { PushMutes, roomMuted } from "../push/mutes.ts";
|
|
@@ -24,6 +37,7 @@ import { PushSender } from "../push/sender.ts";
|
|
|
24
37
|
import { PushSubscriptions } from "../push/subscriptions.ts";
|
|
25
38
|
import { PushService } from "../push/triggers.ts";
|
|
26
39
|
import type { Vapid } from "../push/vapid.ts";
|
|
40
|
+
import type { WebAppRegistry } from "../web-app.ts";
|
|
27
41
|
import { CardLog } from "./card-log.ts";
|
|
28
42
|
import { OwnerDesk } from "./desk.ts";
|
|
29
43
|
import { FileDir } from "./files.ts";
|
|
@@ -33,7 +47,7 @@ import { RoomLog } from "./room-log.ts";
|
|
|
33
47
|
import { Rooms } from "./rooms.ts";
|
|
34
48
|
import { AppApi, type AppApiOptions, partsOf } from "./routes.ts";
|
|
35
49
|
import { SocketRoute } from "./socket.ts";
|
|
36
|
-
import { type AppSocket, Sockets } from "./sockets.ts";
|
|
50
|
+
import { type AppSocket, type Audience, Sockets } from "./sockets.ts";
|
|
37
51
|
import { AppSurface } from "./surface.ts";
|
|
38
52
|
import { WebThreads } from "./threads.ts";
|
|
39
53
|
import { Uploads } from "./uploads.ts";
|
|
@@ -47,11 +61,18 @@ const SWEEP_EVERY_MS = 3_600_000;
|
|
|
47
61
|
/** The tables the app keeps; the plugin lists them as its own migrations. */
|
|
48
62
|
export const APP_MIGRATIONS: readonly Migration[] = [
|
|
49
63
|
RoomLog.migration,
|
|
64
|
+
RoomLog.hostMigration,
|
|
65
|
+
RoomLog.noteMigration,
|
|
66
|
+
HostCardLog.migration,
|
|
67
|
+
HostCardLog.keepMigration,
|
|
68
|
+
RoomReads.migration,
|
|
50
69
|
CardLog.migration,
|
|
51
70
|
WebThreads.migration,
|
|
71
|
+
WebThreads.dismissedMigration,
|
|
52
72
|
PushSubscriptions.migration,
|
|
53
73
|
PushMutes.migration,
|
|
54
74
|
RoomLayout.migration,
|
|
75
|
+
...NOTICE_MIGRATIONS,
|
|
55
76
|
];
|
|
56
77
|
|
|
57
78
|
export interface OwnerAppOptions {
|
|
@@ -69,14 +90,36 @@ export interface OwnerAppOptions {
|
|
|
69
90
|
cardGraceSeconds: number;
|
|
70
91
|
/** The most bytes of one uploaded file. */
|
|
71
92
|
uploadMaxBytes: number;
|
|
72
|
-
|
|
93
|
+
/** How many days a stored file of an agent room is kept; default 30. */
|
|
94
|
+
uploadKeepDays?: number;
|
|
95
|
+
/** The name the app shows for a person; default the principal's own. */
|
|
96
|
+
nameOf?(principalId: string): Promise<string>;
|
|
97
|
+
/** The same-origin URL of a room, and of one of its messages, for a host's links. */
|
|
98
|
+
roomUrl(room: ChannelKey, message?: number): string;
|
|
99
|
+
/** Where the app reports what people do in it (a press on a host's button). */
|
|
100
|
+
events?: Pick<EventSink, "action">;
|
|
101
|
+
authenticator: Pick<Authenticator, "authenticate" | "recheck">;
|
|
73
102
|
/** How the console signs the person in. */
|
|
74
|
-
authMode: "
|
|
103
|
+
authMode: ResolvedOptions["auth"]["mode"];
|
|
104
|
+
/** Who the app admits; the page and the session report the tier only where it admits more than owners. */
|
|
105
|
+
admission: ResolvedOptions["admission"];
|
|
106
|
+
/** What the pieces of the app plug in: the kinds of host room, their handlers, who reads them. */
|
|
107
|
+
registry: WebAppRegistry;
|
|
108
|
+
/** The onboarding gate. */
|
|
109
|
+
gate: Gate;
|
|
110
|
+
/** The tiers whose people may use push. */
|
|
111
|
+
pushTiers: ResolvedOptions["push"]["tiers"];
|
|
112
|
+
/** Whether the yolo and long-running switches are served. */
|
|
113
|
+
modeSwitches: boolean;
|
|
75
114
|
/** The longest an open socket lives; for a credential that logout or a new password does not reach. */
|
|
76
115
|
socketLifeMs?: number;
|
|
77
|
-
/**
|
|
116
|
+
/** The notices, onboarding and profile pieces: what they read of the options. */
|
|
117
|
+
extras: Pick<NoticesMountOptions, "profiles" | "routing" | "onboarding"> & {
|
|
118
|
+
push: NoticesMountOptions["push"];
|
|
119
|
+
};
|
|
120
|
+
/** Web Push: the server's key (absent with an external sender, which holds it), and how notifications read. */
|
|
78
121
|
push: {
|
|
79
|
-
vapid
|
|
122
|
+
vapid?: Vapid;
|
|
80
123
|
/** The `sub` of the VAPID token; absent = push is not set up and nothing is sent. */
|
|
81
124
|
subject: string | undefined;
|
|
82
125
|
preview: boolean;
|
|
@@ -124,10 +167,12 @@ export class OwnerApp {
|
|
|
124
167
|
readonly directChannel: DirectChannelProvider;
|
|
125
168
|
readonly #api: AppApi;
|
|
126
169
|
readonly #push: PushApi;
|
|
170
|
+
readonly #mounted: NoticesMount;
|
|
127
171
|
readonly #sockets: Sockets;
|
|
128
172
|
readonly #desk: OwnerDesk;
|
|
129
173
|
readonly #presence: Presence;
|
|
130
174
|
readonly #uploads: Uploads;
|
|
175
|
+
readonly #host: HostApp;
|
|
131
176
|
readonly #options: OwnerAppOptions;
|
|
132
177
|
readonly #unsubscribe: (() => void)[] = [];
|
|
133
178
|
#sweeper: ReturnType<typeof setInterval> | undefined;
|
|
@@ -143,7 +188,17 @@ export class OwnerApp {
|
|
|
143
188
|
const waiting = new FileDir(join(options.dataDir, "web", "uploads"));
|
|
144
189
|
const uploads = new Uploads(waiting, stored, {
|
|
145
190
|
maxBytes: options.uploadMaxBytes,
|
|
191
|
+
...(options.uploadKeepDays === undefined
|
|
192
|
+
? {}
|
|
193
|
+
: { keepDays: options.uploadKeepDays }),
|
|
146
194
|
});
|
|
195
|
+
const reads = new RoomReads(options.database);
|
|
196
|
+
const hostCards = new HostCardLog(options.database);
|
|
197
|
+
const nameOf =
|
|
198
|
+
options.nameOf ??
|
|
199
|
+
(async (id: string) => (await identity.principal(id))?.displayName ?? id);
|
|
200
|
+
// The host rooms read their context once the surface exists; the surface asks for it per turn.
|
|
201
|
+
let hostApp: HostApp | undefined;
|
|
147
202
|
const threads = new WebThreads(options.database);
|
|
148
203
|
const subscriptions = new PushSubscriptions(options.database);
|
|
149
204
|
const mutes = new PushMutes(options.database);
|
|
@@ -151,34 +206,63 @@ export class OwnerApp {
|
|
|
151
206
|
const quietClock = options.push.quietHours
|
|
152
207
|
? new QuietClock(options.push.quietHours, options.push.timeZone)
|
|
153
208
|
: undefined;
|
|
209
|
+
const quiet = quietClock
|
|
210
|
+
? (at: number) => quietClock.quietAt(at)
|
|
211
|
+
: undefined;
|
|
212
|
+
const mounted = mountNotices({
|
|
213
|
+
database: options.database,
|
|
214
|
+
logger,
|
|
215
|
+
registry: options.registry,
|
|
216
|
+
gate: options.gate,
|
|
217
|
+
sockets,
|
|
218
|
+
identity,
|
|
219
|
+
...(options.extras.profiles ? { profiles: options.extras.profiles } : {}),
|
|
220
|
+
subscriptions,
|
|
221
|
+
policy,
|
|
222
|
+
mount: options.mount,
|
|
223
|
+
routing: options.extras.routing,
|
|
224
|
+
push: options.extras.push,
|
|
225
|
+
...(quiet ? { quiet } : {}),
|
|
226
|
+
...(options.push.vapid ? { vapid: options.push.vapid } : {}),
|
|
227
|
+
onboarding: options.extras.onboarding,
|
|
228
|
+
});
|
|
154
229
|
const push = new PushService({
|
|
230
|
+
hostRoom: (room) => options.registry.isHostRoom(room),
|
|
155
231
|
subscriptions,
|
|
156
|
-
sender
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
232
|
+
// The app signs only with its own key; with an external sender it queues for it instead.
|
|
233
|
+
...(options.push.vapid
|
|
234
|
+
? {
|
|
235
|
+
sender: new PushSender({
|
|
236
|
+
vapid: options.push.vapid,
|
|
237
|
+
subject: options.push.subject,
|
|
238
|
+
policy,
|
|
239
|
+
...(options.push.fetch ? { fetch: options.push.fetch } : {}),
|
|
240
|
+
logger,
|
|
241
|
+
}),
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
...(mounted.route ? { route: mounted.route } : {}),
|
|
163
245
|
identity,
|
|
164
246
|
visible: (principalId) => sockets.anyVisible(principalId),
|
|
165
247
|
preview: options.push.preview,
|
|
166
|
-
...(
|
|
248
|
+
...(quiet ? { quiet } : {}),
|
|
167
249
|
muted: roomMuted(mutes, threads, channelKey(options.surface, "thread.")),
|
|
168
250
|
urlOf: options.push.urlOf,
|
|
169
251
|
logger,
|
|
170
252
|
});
|
|
171
|
-
this.#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
253
|
+
this.#mounted = mounted;
|
|
254
|
+
this.#push = mounted.pushApi;
|
|
255
|
+
const { registry } = options;
|
|
256
|
+
sockets.setRoomReader((connection, room) =>
|
|
257
|
+
registry.access.readNow(connection, room),
|
|
258
|
+
);
|
|
259
|
+
// An agent room's frames are the owners'; a host room's are its readers'.
|
|
260
|
+
const audienceOf = (room: string): Audience =>
|
|
261
|
+
registry.isHostRoom(room) ? { room } : { owners: true };
|
|
178
262
|
// The desk starts a late answer's turn through the socket route, which is made after it.
|
|
179
263
|
let route: SocketRoute | undefined;
|
|
180
264
|
const desk = new OwnerDesk({
|
|
181
|
-
broadcast: (frame) => sockets.
|
|
265
|
+
broadcast: (frame, room) => sockets.deliver(frame, audienceOf(room)),
|
|
182
266
|
graceMs: options.cardGraceSeconds * 1000,
|
|
183
267
|
closed: (card) => cards.record(card),
|
|
184
268
|
opened: (room, frame) => {
|
|
@@ -213,6 +297,20 @@ export class OwnerApp {
|
|
|
213
297
|
const room = await rooms.resolve(parent);
|
|
214
298
|
return room !== undefined && room.kind !== "thread";
|
|
215
299
|
},
|
|
300
|
+
audienceOf,
|
|
301
|
+
isHostRoom: (room) => registry.isHostRoom(room),
|
|
302
|
+
fileBinding: (room) => {
|
|
303
|
+
const kind = registry.hostKindOf(room)?.kind;
|
|
304
|
+
return {
|
|
305
|
+
room,
|
|
306
|
+
...(kind?.retention
|
|
307
|
+
? { expiresAt: Date.now() + kind.retention.fileDays * 86_400_000 }
|
|
308
|
+
: {}),
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
channelContext: (message, contextOptions) =>
|
|
312
|
+
hostApp?.channelContext(message, contextOptions) ??
|
|
313
|
+
Promise.resolve(undefined),
|
|
216
314
|
changed: () => this.changed(),
|
|
217
315
|
unprompted: (room, author, text) => {
|
|
218
316
|
push.reply(room, author, text).catch(() => undefined);
|
|
@@ -237,7 +335,30 @@ export class OwnerApp {
|
|
|
237
335
|
modes,
|
|
238
336
|
mutes,
|
|
239
337
|
activity: (room) => surface.activityOf(room),
|
|
338
|
+
// The agent rooms are the owners'; where there are several, they can leave each other notes.
|
|
339
|
+
readers: async () => (await identity.owners()).length > 1,
|
|
240
340
|
});
|
|
341
|
+
hostApp = new HostApp({
|
|
342
|
+
surface: options.surface,
|
|
343
|
+
registry,
|
|
344
|
+
sockets,
|
|
345
|
+
app: surface,
|
|
346
|
+
desk,
|
|
347
|
+
log,
|
|
348
|
+
cards: hostCards,
|
|
349
|
+
reads,
|
|
350
|
+
files: stored,
|
|
351
|
+
uploads,
|
|
352
|
+
rooms,
|
|
353
|
+
conversations: options.conversations,
|
|
354
|
+
nameOf,
|
|
355
|
+
roomUrl: options.roomUrl,
|
|
356
|
+
maxFileBytes: options.uploadMaxBytes,
|
|
357
|
+
...(options.events ? { events: options.events } : {}),
|
|
358
|
+
logger,
|
|
359
|
+
});
|
|
360
|
+
hostApp.install();
|
|
361
|
+
this.#host = hostApp;
|
|
241
362
|
route = new SocketRoute({
|
|
242
363
|
path: `${options.mount}/api/socket`,
|
|
243
364
|
origin: options.origin,
|
|
@@ -251,10 +372,17 @@ export class OwnerApp {
|
|
|
251
372
|
rooms,
|
|
252
373
|
uploads,
|
|
253
374
|
presence,
|
|
254
|
-
|
|
375
|
+
registry,
|
|
376
|
+
gate: options.gate,
|
|
377
|
+
audienceOf,
|
|
378
|
+
opened: (connection) => mounted.opened(connection),
|
|
255
379
|
conversations: options.conversations,
|
|
256
380
|
logger,
|
|
257
381
|
});
|
|
382
|
+
const socketRoute = route;
|
|
383
|
+
this.#unsubscribe.push(
|
|
384
|
+
options.gate.onChanged((principalId) => socketRoute.refresh(principalId)),
|
|
385
|
+
);
|
|
258
386
|
this.websocket = route.route();
|
|
259
387
|
this.directChannel = webPushChannel({
|
|
260
388
|
surface: options.surface,
|
|
@@ -291,8 +419,16 @@ export class OwnerApp {
|
|
|
291
419
|
...(options.skills ? { skills: options.skills } : {}),
|
|
292
420
|
...(options.prompts ? { prompts: options.prompts } : {}),
|
|
293
421
|
avatars: agents.avatars,
|
|
422
|
+
reads,
|
|
423
|
+
host: hostApp,
|
|
424
|
+
nameOf,
|
|
294
425
|
mount: options.mount,
|
|
295
426
|
authMode: options.authMode,
|
|
427
|
+
admission: options.admission,
|
|
428
|
+
registry,
|
|
429
|
+
gate: options.gate,
|
|
430
|
+
pushTiers: options.pushTiers,
|
|
431
|
+
modeSwitches: options.modeSwitches,
|
|
296
432
|
...(options.push.quietHours
|
|
297
433
|
? {
|
|
298
434
|
quietHours: {
|
|
@@ -312,12 +448,15 @@ export class OwnerApp {
|
|
|
312
448
|
this.#unsubscribe.push(
|
|
313
449
|
modes.onChange((channel, mode) => {
|
|
314
450
|
if (!rooms.ownsKey(channel)) return;
|
|
315
|
-
sockets.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
451
|
+
sockets.deliver(
|
|
452
|
+
{
|
|
453
|
+
type: "mode",
|
|
454
|
+
room: channel,
|
|
455
|
+
yolo: mode.yolo,
|
|
456
|
+
longRunning: mode.longRunning,
|
|
457
|
+
},
|
|
458
|
+
{ owners: true },
|
|
459
|
+
);
|
|
321
460
|
}),
|
|
322
461
|
modes.onAutoApproved((entry) => {
|
|
323
462
|
if (!rooms.ownsKey(entry.channel)) return;
|
|
@@ -342,17 +481,28 @@ export class OwnerApp {
|
|
|
342
481
|
return pushed ?? this.#api.handle(request, rest, admission);
|
|
343
482
|
}
|
|
344
483
|
|
|
345
|
-
/**
|
|
484
|
+
/** Closes the sockets that came in on a session that has ended. */
|
|
485
|
+
sessionEnded(id: string): void {
|
|
486
|
+
this.#sockets.closeWhere(
|
|
487
|
+
(connection) => connection.session === id,
|
|
488
|
+
APP_CLOSE_EXPIRED,
|
|
489
|
+
"expired",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Tells the owners' sockets the team or the room list may differ, once changes settle. */
|
|
346
494
|
changed(): void {
|
|
347
495
|
if (this.#pending) return;
|
|
348
496
|
this.#pending = setTimeout(() => {
|
|
349
497
|
this.#pending = undefined;
|
|
350
|
-
this.#sockets.
|
|
498
|
+
this.#sockets.deliver({ type: "changed" }, { owners: true });
|
|
351
499
|
}, CHANGED_COALESCE_MS);
|
|
352
500
|
}
|
|
353
501
|
|
|
354
502
|
start(): void {
|
|
355
503
|
this.#uploads.adopt();
|
|
504
|
+
this.#host.start();
|
|
505
|
+
this.#mounted.start();
|
|
356
506
|
this.#sweep();
|
|
357
507
|
this.#sweeper = setInterval(() => this.#sweep(), SWEEP_EVERY_MS);
|
|
358
508
|
}
|
|
@@ -364,6 +514,8 @@ export class OwnerApp {
|
|
|
364
514
|
clearTimeout(this.#pending);
|
|
365
515
|
this.#pending = undefined;
|
|
366
516
|
for (const unsubscribe of this.#unsubscribe.splice(0)) unsubscribe();
|
|
517
|
+
this.#host.stop();
|
|
518
|
+
this.#mounted.stop().catch(() => undefined);
|
|
367
519
|
this.#desk.stop();
|
|
368
520
|
this.#presence.stop();
|
|
369
521
|
this.#sockets.closeAll(1001, "stopping");
|
|
@@ -377,6 +529,20 @@ export class OwnerApp {
|
|
|
377
529
|
} catch (error) {
|
|
378
530
|
this.#options.logger.warn({ err: error }, "old app files not deleted");
|
|
379
531
|
}
|
|
532
|
+
this.#host.sweep().then(
|
|
533
|
+
(gone) => {
|
|
534
|
+
if (gone.messages + gone.cards + gone.files > 0)
|
|
535
|
+
this.#options.logger.info(
|
|
536
|
+
gone,
|
|
537
|
+
"host room history past retention deleted",
|
|
538
|
+
);
|
|
539
|
+
},
|
|
540
|
+
(error: unknown) =>
|
|
541
|
+
this.#options.logger.warn(
|
|
542
|
+
{ err: error },
|
|
543
|
+
"host room history not swept",
|
|
544
|
+
),
|
|
545
|
+
);
|
|
380
546
|
}
|
|
381
547
|
|
|
382
548
|
/** Records and shows that yolo mode approved a held call in a room. */
|
|
@@ -391,6 +557,9 @@ export class OwnerApp {
|
|
|
391
557
|
author: "Yolo mode",
|
|
392
558
|
text: `Yolo mode approved: ${action}`,
|
|
393
559
|
});
|
|
394
|
-
this.#sockets.
|
|
560
|
+
this.#sockets.deliver(
|
|
561
|
+
{ type: "auto_approved", room, tool, action },
|
|
562
|
+
{ owners: true },
|
|
563
|
+
);
|
|
395
564
|
}
|
|
396
565
|
}
|
package/src/app/desk.ts
CHANGED
|
@@ -50,13 +50,15 @@ export interface Answerer {
|
|
|
50
50
|
/** The identity they spoke as, `<provider>:<subject>`. */
|
|
51
51
|
speakerId: string;
|
|
52
52
|
name: string;
|
|
53
|
+
/** The roles their sign-in carried. */
|
|
54
|
+
roles?: readonly string[];
|
|
53
55
|
/** The actor their credential vouched for, which a message they start is written as. */
|
|
54
56
|
actor?: ActorFacts;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export interface OwnerDeskOptions {
|
|
58
|
-
/**
|
|
59
|
-
broadcast(frame: ServerFrame): void;
|
|
60
|
+
/** Puts a frame about a room on the sockets that may have it: the owners' for an agent room, the room's readers' for a host room. */
|
|
61
|
+
broadcast(frame: ServerFrame, room: ChannelKey): void;
|
|
60
62
|
/** How long a turn that can be resumed waits on its card before it goes on without the answer. */
|
|
61
63
|
graceMs: number;
|
|
62
64
|
/** A card nobody answers closes after this long; default 24 hours. */
|
|
@@ -143,9 +145,14 @@ function answerOf(ended: Ended): CardAnswer | undefined {
|
|
|
143
145
|
* after a day. A turn that can be resumed stops waiting after the grace period while the card
|
|
144
146
|
* stays open (`late`); answered then, the card starts a turn in its room with the answer.
|
|
145
147
|
*
|
|
146
|
-
* Who answers
|
|
147
|
-
*
|
|
148
|
-
*
|
|
148
|
+
* Who answers (the same rule as Discord's `CardAudiences`, so a room's readers cannot widen it):
|
|
149
|
+
* - an approval that names approvers: a named approver who holds its tier now, or an owner;
|
|
150
|
+
* - an approval that names none: the turn's speaker at its tier, and the owners when the scope
|
|
151
|
+
* escalates;
|
|
152
|
+
* - a question: the speaker, and the owners when the scope escalates;
|
|
153
|
+
* - a card that is the speaker's alone (`speakerOnly`): only the speaker's principal;
|
|
154
|
+
* - without a scope: the owners.
|
|
155
|
+
* It applies to approve, decline, answer and cancel alike, and the tier is the answerer's now.
|
|
149
156
|
*/
|
|
150
157
|
export class OwnerDesk {
|
|
151
158
|
readonly #open = new Map<string, Open>();
|
|
@@ -273,15 +280,19 @@ export class OwnerDesk {
|
|
|
273
280
|
};
|
|
274
281
|
}
|
|
275
282
|
|
|
276
|
-
/** The open cards as frames, to send again when a socket opens. */
|
|
277
|
-
openFrames(): ServerFrame[] {
|
|
283
|
+
/** The open cards as frames with their rooms, to send again when a socket opens; each goes only to those who may have its room. */
|
|
284
|
+
openFrames(): { room: ChannelKey; frame: ServerFrame }[] {
|
|
278
285
|
return [...this.#open.values()].map((open) => ({
|
|
279
|
-
type: "prompt",
|
|
280
286
|
room: open.room,
|
|
281
|
-
prompt: open.frame,
|
|
287
|
+
frame: { type: "prompt", room: open.room, prompt: open.frame },
|
|
282
288
|
}));
|
|
283
289
|
}
|
|
284
290
|
|
|
291
|
+
/** The room a card belongs to, while it is open. */
|
|
292
|
+
roomOf(prompt: string): ChannelKey | undefined {
|
|
293
|
+
return this.#open.get(prompt)?.room;
|
|
294
|
+
}
|
|
295
|
+
|
|
285
296
|
/** Whether the card is open and past its grace, so an answer to it starts a turn. */
|
|
286
297
|
isLate(prompt: string): boolean {
|
|
287
298
|
const frame = this.#open.get(prompt)?.frame;
|
|
@@ -357,7 +368,7 @@ export class OwnerDesk {
|
|
|
357
368
|
|
|
358
369
|
/**
|
|
359
370
|
* Who may approve a call held at `minTier`, as a test of the person who answers; undefined when
|
|
360
|
-
* no one may, so the call is unavailable at once.
|
|
371
|
+
* no one may, so the call is unavailable at once.
|
|
361
372
|
*/
|
|
362
373
|
#approvalAudience(
|
|
363
374
|
scope: PromptScope | undefined,
|
|
@@ -366,6 +377,7 @@ export class OwnerDesk {
|
|
|
366
377
|
speakerOnly: boolean,
|
|
367
378
|
): ((from: Answerer) => boolean) | undefined {
|
|
368
379
|
const tierOk = (from: Answerer) => atLeast(from.tier, minTier);
|
|
380
|
+
const owner = (from: Answerer) => from.tier === "owner";
|
|
369
381
|
if (speakerOnly) {
|
|
370
382
|
// The speaker's alone: below the tier, or a speaker who is no person, no one answers.
|
|
371
383
|
if (
|
|
@@ -377,28 +389,38 @@ export class OwnerDesk {
|
|
|
377
389
|
const speaker = scope.principalId;
|
|
378
390
|
return (from) => from.principalId === speaker && tierOk(from);
|
|
379
391
|
}
|
|
380
|
-
|
|
392
|
+
// Without a scope a card is the owners'.
|
|
393
|
+
if (!scope) return owner;
|
|
394
|
+
const person = scope.principalId;
|
|
395
|
+
if (scope.escalate === "none") {
|
|
381
396
|
// A private conversation's person is the only one who answers, and a call naming others is theirs only if named or an owner.
|
|
382
397
|
if (!atLeast(scope.tier, minTier)) return undefined;
|
|
383
398
|
if (
|
|
384
399
|
approvers?.length &&
|
|
385
|
-
!approvers.includes(
|
|
400
|
+
!approvers.includes(person) &&
|
|
386
401
|
scope.tier !== "owner"
|
|
387
402
|
)
|
|
388
403
|
return undefined;
|
|
389
|
-
const person = scope.principalId;
|
|
390
404
|
return (from) => from.principalId === person && tierOk(from);
|
|
391
405
|
}
|
|
392
|
-
|
|
406
|
+
if (approvers?.length) {
|
|
407
|
+
// Named approvers answer it, at the tier it needs now, and so do the owners; the speaker does not unless named.
|
|
408
|
+
const named = new Set(approvers);
|
|
409
|
+
return (from) =>
|
|
410
|
+
(named.has(from.principalId) && tierOk(from)) || owner(from);
|
|
411
|
+
}
|
|
412
|
+
return (from) =>
|
|
413
|
+
(from.principalId === person && tierOk(from)) || owner(from);
|
|
393
414
|
}
|
|
394
415
|
|
|
395
|
-
/** Who may answer a question:
|
|
416
|
+
/** Who may answer a question: the speaker, and the owners where the scope escalates; without a scope, the owners. */
|
|
396
417
|
#questionAudience(scope: PromptScope | undefined) {
|
|
397
|
-
if (scope
|
|
398
|
-
|
|
418
|
+
if (!scope) return (from: Answerer) => from.tier === "owner";
|
|
419
|
+
const person = scope.principalId;
|
|
420
|
+
if (scope.escalate === "none")
|
|
399
421
|
return (from: Answerer) => from.principalId === person;
|
|
400
|
-
|
|
401
|
-
|
|
422
|
+
return (from: Answerer) =>
|
|
423
|
+
from.principalId === person || from.tier === "owner";
|
|
402
424
|
}
|
|
403
425
|
|
|
404
426
|
/**
|
|
@@ -463,7 +485,10 @@ export class OwnerDesk {
|
|
|
463
485
|
};
|
|
464
486
|
// Kept before the sockets are told, so a page that reloads on the news finds it.
|
|
465
487
|
this.#keep({ room, frame: open.frame, ...ended, closedAt });
|
|
466
|
-
broadcast(
|
|
488
|
+
broadcast(
|
|
489
|
+
{ type: "prompt_closed", prompt: id, ...ended, closedAt },
|
|
490
|
+
room,
|
|
491
|
+
);
|
|
467
492
|
};
|
|
468
493
|
const onAbort = () => {
|
|
469
494
|
if (!this.#open.has(id)) return;
|
|
@@ -488,13 +513,13 @@ export class OwnerDesk {
|
|
|
488
513
|
late = true;
|
|
489
514
|
signal?.removeEventListener("abort", onAbort);
|
|
490
515
|
open.frame = { ...open.frame, late: true } as PromptFrame;
|
|
491
|
-
broadcast({ type: "prompt", room, prompt: open.frame });
|
|
516
|
+
broadcast({ type: "prompt", room, prompt: open.frame }, room);
|
|
492
517
|
resolve(card.pending as T);
|
|
493
518
|
}, graceMs);
|
|
494
519
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
495
520
|
supersede?.addEventListener("abort", onWithdraw, { once: true });
|
|
496
521
|
this.#open.set(id, open);
|
|
497
|
-
broadcast({ type: "prompt", room, prompt: open.frame });
|
|
522
|
+
broadcast({ type: "prompt", room, prompt: open.frame }, room);
|
|
498
523
|
try {
|
|
499
524
|
this.#options.opened?.(room, open.frame);
|
|
500
525
|
} catch (error) {
|
package/src/app/files.ts
CHANGED
|
@@ -73,6 +73,16 @@ interface Meta {
|
|
|
73
73
|
name: string;
|
|
74
74
|
type: string;
|
|
75
75
|
size: number;
|
|
76
|
+
/** The host room the file is bound to: only someone who may read that room is served it. */
|
|
77
|
+
room?: string;
|
|
78
|
+
/** Epoch milliseconds after which the sweep deletes the file; absent = the file is kept. */
|
|
79
|
+
expiresAt?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What binds a stored file to a host room. */
|
|
83
|
+
export interface FileBinding {
|
|
84
|
+
room: string;
|
|
85
|
+
expiresAt?: number;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
/** A file being written as its bytes arrive; `finish` makes it a stored file, `abort` leaves nothing. */
|
|
@@ -85,6 +95,13 @@ export interface FileWriter {
|
|
|
85
95
|
abort(): Promise<void>;
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
const bindingFields = (
|
|
99
|
+
binding: FileBinding,
|
|
100
|
+
): Pick<Meta, "room" | "expiresAt"> => ({
|
|
101
|
+
room: binding.room,
|
|
102
|
+
...(binding.expiresAt === undefined ? {} : { expiresAt: binding.expiresAt }),
|
|
103
|
+
});
|
|
104
|
+
|
|
88
105
|
/**
|
|
89
106
|
* A directory of files kept by id, each with a small record of its name and type beside it.
|
|
90
107
|
* Both stores of the app, the files of replies and sent messages and the uploads waiting for a
|
|
@@ -101,20 +118,61 @@ export class FileDir {
|
|
|
101
118
|
return join(this.dir, id);
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
/** Keeps the bytes under a new id. */
|
|
121
|
+
/** Keeps the bytes under a new id, bound to a host room when `binding` is given. */
|
|
105
122
|
put(
|
|
106
123
|
name: string,
|
|
107
124
|
type: string,
|
|
108
125
|
bytes: Uint8Array,
|
|
109
126
|
id = crypto.randomUUID(),
|
|
127
|
+
binding?: FileBinding,
|
|
110
128
|
): RoomFile {
|
|
111
129
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
112
|
-
const meta: Meta = {
|
|
130
|
+
const meta: Meta = {
|
|
131
|
+
name: safeName(name),
|
|
132
|
+
type,
|
|
133
|
+
size: bytes.byteLength,
|
|
134
|
+
...(binding ? bindingFields(binding) : {}),
|
|
135
|
+
};
|
|
113
136
|
writeFileSync(this.#path(id), bytes, { mode: 0o600 });
|
|
114
137
|
writeFileSync(`${this.#path(id)}.json`, JSON.stringify(meta), {
|
|
115
138
|
mode: 0o600,
|
|
116
139
|
});
|
|
117
|
-
return { id,
|
|
140
|
+
return { id, name: meta.name, type, size: meta.size };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#read(id: string): Meta | undefined {
|
|
144
|
+
if (!isFileId(id)) return undefined;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(readFileSync(`${this.#path(id)}.json`, "utf8")) as Meta;
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The room a file is bound to and when it expires; undefined for a file that is bound to none. */
|
|
153
|
+
binding(id: string): FileBinding | undefined {
|
|
154
|
+
const meta = this.#read(id);
|
|
155
|
+
return meta?.room === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: {
|
|
158
|
+
room: meta.room,
|
|
159
|
+
...(meta.expiresAt === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { expiresAt: meta.expiresAt }),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Binds a stored file to a host room, or changes its expiry; false when there is no such file. */
|
|
166
|
+
bind(id: string, binding: FileBinding): boolean {
|
|
167
|
+
const meta = this.#read(id);
|
|
168
|
+
if (!meta || !existsSync(this.#path(id))) return false;
|
|
169
|
+
const { room: _room, expiresAt: _expiresAt, ...plain } = meta;
|
|
170
|
+
writeFileSync(
|
|
171
|
+
`${this.#path(id)}.json`,
|
|
172
|
+
JSON.stringify({ ...plain, ...bindingFields(binding) }),
|
|
173
|
+
{ mode: 0o600 },
|
|
174
|
+
);
|
|
175
|
+
return true;
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
/** Starts a file under a new id whose bytes arrive in pieces, so it is never held whole. */
|
|
@@ -161,12 +219,20 @@ export class FileDir {
|
|
|
161
219
|
}
|
|
162
220
|
|
|
163
221
|
meta(id: string): RoomFile | undefined {
|
|
222
|
+
const meta = this.#read(id);
|
|
223
|
+
return meta && { id, name: meta.name, type: meta.type, size: meta.size };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Whether the file's bytes are still stored. */
|
|
227
|
+
has(id: string): boolean {
|
|
228
|
+
return isFileId(id) && existsSync(this.#path(id));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** How long ago the file was written, in milliseconds; undefined when there is none. */
|
|
232
|
+
ageOf(id: string, now: number): number | undefined {
|
|
164
233
|
if (!isFileId(id)) return undefined;
|
|
165
234
|
try {
|
|
166
|
-
|
|
167
|
-
readFileSync(`${this.#path(id)}.json`, "utf8"),
|
|
168
|
-
) as Meta;
|
|
169
|
-
return { id, name: meta.name, type: meta.type, size: meta.size };
|
|
235
|
+
return now - statSync(this.#path(id)).mtimeMs;
|
|
170
236
|
} catch {
|
|
171
237
|
return undefined;
|
|
172
238
|
}
|
package/src/app/presence.ts
CHANGED
|
@@ -46,7 +46,7 @@ export class Presence {
|
|
|
46
46
|
return { type: "presence", agents };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** Sends the table to
|
|
49
|
+
/** Sends the table to the owners' sockets once changes settle, if it changed. */
|
|
50
50
|
schedule(): void {
|
|
51
51
|
if (this.#pending || this.#stopped) return;
|
|
52
52
|
this.#pending = setTimeout(() => {
|
|
@@ -62,7 +62,7 @@ export class Presence {
|
|
|
62
62
|
const text = JSON.stringify(frame.agents);
|
|
63
63
|
if (text === this.#last || this.#stopped) return;
|
|
64
64
|
this.#last = text;
|
|
65
|
-
this.#options.sockets.
|
|
65
|
+
this.#options.sockets.deliver(frame, { owners: true });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
stop(): void {
|