app.ts 3.4 KB

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