app.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import "zod-openapi/extend"
  2. import { Log } from "../util/log"
  3. import { Context } from "../util/context"
  4. import { Filesystem } from "../util/filesystem"
  5. import { Global } from "../global"
  6. import path from "path"
  7. import os from "os"
  8. import { z } from "zod"
  9. export namespace App {
  10. const log = Log.create({ service: "app" })
  11. export const Info = z
  12. .object({
  13. hostname: z.string(),
  14. git: z.boolean(),
  15. path: z.object({
  16. home: z.string(),
  17. config: z.string(),
  18. data: z.string(),
  19. root: z.string(),
  20. cwd: z.string(),
  21. state: z.string(),
  22. }),
  23. time: z.object({
  24. initialized: z.number().optional(),
  25. }),
  26. })
  27. .openapi({
  28. ref: "App",
  29. })
  30. export type Info = z.infer<typeof Info>
  31. const ctx = Context.create<{
  32. info: Info
  33. services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
  34. }>("app")
  35. export const use = ctx.use
  36. const APP_JSON = "app.json"
  37. export type Input = {
  38. cwd: string
  39. }
  40. export const provideExisting = ctx.provide
  41. export async function provide<T>(input: Input, cb: (app: App.Info) => Promise<T>) {
  42. log.info("creating", {
  43. cwd: input.cwd,
  44. })
  45. const git = await Filesystem.findUp(".git", input.cwd).then(([x]) => (x ? path.dirname(x) : undefined))
  46. log.info("git", { git })
  47. const data = path.join(Global.Path.data, "project", git ? directory(git) : "global")
  48. const stateFile = Bun.file(path.join(data, APP_JSON))
  49. const state = (await stateFile.json().catch(() => ({}))) as {
  50. initialized: number
  51. }
  52. await stateFile.write(JSON.stringify(state))
  53. const services = new Map<
  54. any,
  55. {
  56. state: any
  57. shutdown?: (input: any) => Promise<void>
  58. }
  59. >()
  60. const root = git ?? input.cwd
  61. const info: Info = {
  62. hostname: os.hostname(),
  63. time: {
  64. initialized: state.initialized,
  65. },
  66. git: git !== undefined,
  67. path: {
  68. home: os.homedir(),
  69. config: Global.Path.config,
  70. state: Global.Path.state,
  71. data,
  72. root,
  73. cwd: input.cwd,
  74. },
  75. }
  76. const app = {
  77. services,
  78. info,
  79. }
  80. return ctx.provide(app, async () => {
  81. try {
  82. const result = await cb(app.info)
  83. return result
  84. } finally {
  85. for (const [key, entry] of app.services.entries()) {
  86. if (!entry.shutdown) continue
  87. log.info("shutdown", { name: key })
  88. await entry.shutdown?.(await entry.state)
  89. }
  90. }
  91. })
  92. }
  93. export function state<State>(
  94. key: any,
  95. init: (app: Info) => State,
  96. shutdown?: (state: Awaited<State>) => Promise<void>,
  97. ) {
  98. return () => {
  99. const app = ctx.use()
  100. const services = app.services
  101. if (!services.has(key)) {
  102. log.info("registering service", { name: key })
  103. services.set(key, {
  104. state: init(app.info),
  105. shutdown,
  106. })
  107. }
  108. return services.get(key)?.state as State
  109. }
  110. }
  111. export function info() {
  112. return ctx.use().info
  113. }
  114. export async function initialize() {
  115. const { info } = ctx.use()
  116. info.time.initialized = Date.now()
  117. await Bun.write(
  118. path.join(info.path.data, APP_JSON),
  119. JSON.stringify({
  120. initialized: Date.now(),
  121. }),
  122. )
  123. }
  124. function directory(input: string): string {
  125. return input
  126. .split(path.sep)
  127. .filter(Boolean)
  128. .join("-")
  129. .replace(/[^A-Za-z0-9_]/g, "-")
  130. }
  131. }