1+ import type { DatabaseStorageStatus } from "@t3tools/contracts" ;
12import * as Context from "effect/Context" ;
23import * as Effect from "effect/Effect" ;
4+ import * as Equal from "effect/Equal" ;
35import * as Layer from "effect/Layer" ;
6+ import * as Path from "effect/Path" ;
47import * as Schedule from "effect/Schedule" ;
8+ import * as Stream from "effect/Stream" ;
9+ import * as SubscriptionRef from "effect/SubscriptionRef" ;
510import * as SqlClient from "effect/sql/SqlClient" ;
611
12+ import { resolveHostCliCommand } from "../cli/invocation.ts" ;
13+ import * as ServerConfig from "../config.ts" ;
714import * as ProjectionMaintenance from "../orchestration-v2/ProjectionMaintenance.ts" ;
815import { forkParked } from "../serverActivation.ts" ;
916
@@ -15,6 +22,8 @@ export class DatabaseStorage extends Context.Service<
1522 {
1623 /** Return free pages to the filesystem on databases with incremental auto_vacuum. */
1724 readonly reclaim : Effect . Effect < void > ;
25+ /** Current file accounting, re-published after each maintenance pass. Null until first read. */
26+ readonly status : Stream . Stream < DatabaseStorageStatus | null > ;
1827 }
1928> ( ) ( "t3/persistence/DatabaseStorage" ) { }
2029
@@ -29,6 +38,42 @@ export const layer = Layer.effect(
2938 Effect . gen ( function * ( ) {
3039 const sql = yield * SqlClient . SqlClient ;
3140 const maintenance = yield * ProjectionMaintenance . ProjectionMaintenanceV2 ;
41+ const config = yield * ServerConfig . ServerConfig ;
42+ const path = yield * Path . Path ;
43+ const statusRef = yield * SubscriptionRef . make < DatabaseStorageStatus | null > ( null ) ;
44+
45+ // The notice shows this verbatim; quote for the host's shell. Desktop
46+ // installs have no `t3` on PATH, so the command names their launcher.
47+ const { command : compactCommand } = yield * resolveHostCliCommand ( "doctor" , [
48+ "--base-dir" ,
49+ config . baseDir ,
50+ ] ) ;
51+ // `t3 doctor --base-dir` repairs <baseDir>/userdata. A dev server without
52+ // an explicit home keeps its state in <baseDir>/dev instead, so there the
53+ // command would target the live install; publish no status at all.
54+ const doctorTargetsThisDatabase =
55+ path . resolve ( config . dbPath ) ===
56+ path . resolve ( config . baseDir , "userdata" , path . basename ( config . dbPath ) ) ;
57+ const publishStatus = Effect . gen ( function * ( ) {
58+ if ( ! doctorTargetsThisDatabase ) return ;
59+ const pages = yield * sql < { readonly page_count : number } > `PRAGMA page_count` ;
60+ const free = yield * sql < { readonly freelist_count : number } > `PRAGMA freelist_count` ;
61+ const size = yield * sql < { readonly page_size : number } > `PRAGMA page_size` ;
62+ const mode = yield * sql < { readonly auto_vacuum : number } > `PRAGMA auto_vacuum` ;
63+ const pageSize = size [ 0 ] ?. page_size ?? 0 ;
64+ const next : DatabaseStorageStatus = {
65+ databaseBytes : ( pages [ 0 ] ?. page_count ?? 0 ) * pageSize ,
66+ reclaimableBytes : ( free [ 0 ] ?. freelist_count ?? 0 ) * pageSize ,
67+ incrementalVacuumEnabled : mode [ 0 ] ?. auto_vacuum === 2 ,
68+ compactCommand,
69+ } ;
70+ // Every set reaches each open Storage page; most passes change nothing.
71+ if ( ! Equal . equals ( yield * SubscriptionRef . get ( statusRef ) , next ) ) {
72+ yield * SubscriptionRef . set ( statusRef , next ) ;
73+ }
74+ } ) . pipe (
75+ Effect . catch ( ( cause ) => Effect . logWarning ( "Database storage inspection failed" , { cause } ) ) ,
76+ ) ;
3277
3378 const reclaim = Effect . gen ( function * ( ) {
3479 const mode = yield * sql < { readonly auto_vacuum : number } > `PRAGMA auto_vacuum` ;
@@ -59,15 +104,20 @@ export const layer = Layer.effect(
59104
60105 yield * forkParked (
61106 Effect . sleep ( "1 minute" ) . pipe (
62- Effect . andThen ( compact . pipe ( Effect . repeat ( Schedule . spaced ( "6 hours" ) ) ) ) ,
107+ Effect . andThen (
108+ compact . pipe ( Effect . andThen ( publishStatus ) , Effect . repeat ( Schedule . spaced ( "6 hours" ) ) ) ,
109+ ) ,
63110 ) ,
64111 ) ;
65112 yield * forkParked (
66- Effect . sleep ( "1 minute" ) . pipe (
67- Effect . andThen ( reclaim . pipe ( Effect . repeat ( Schedule . spaced ( "1 minute" ) ) ) ) ,
113+ publishStatus . pipe (
114+ Effect . andThen ( Effect . sleep ( "1 minute" ) ) ,
115+ Effect . andThen (
116+ reclaim . pipe ( Effect . andThen ( publishStatus ) , Effect . repeat ( Schedule . spaced ( "1 minute" ) ) ) ,
117+ ) ,
68118 ) ,
69119 ) ;
70120
71- return DatabaseStorage . of ( { reclaim } ) ;
121+ return DatabaseStorage . of ( { reclaim, status : SubscriptionRef . changes ( statusRef ) } ) ;
72122 } ) ,
73123) ;
0 commit comments