File size: 3,444 Bytes
b152fd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { existsSync, readFileSync } from "node:fs";

export type PersistedDevServerStatus = {
  dirty: boolean;
  lastChangedAt: string | null;
  changedPathCount: number;
  changedPathsSample: string[];
  pendingMigrations: string[];
  lastRestartAt: string | null;
};

export type DevServerHealthStatus = {
  enabled: true;
  restartRequired: boolean;
  reason: "backend_changes" | "pending_migrations" | "backend_changes_and_pending_migrations" | null;
  lastChangedAt: string | null;
  changedPathCount: number;
  changedPathsSample: string[];
  pendingMigrations: string[];
  autoRestartEnabled: boolean;
  activeRunCount: number;
  waitingForIdle: boolean;
  lastRestartAt: string | null;
};

function normalizeStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  return value
    .filter((entry): entry is string => typeof entry === "string")
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0);
}

function normalizeTimestamp(value: unknown): string | null {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : null;
}

export function readPersistedDevServerStatus(
  env: NodeJS.ProcessEnv = process.env,
): PersistedDevServerStatus | null {
  const filePath = env.PAPERCLIP_DEV_SERVER_STATUS_FILE?.trim();
  if (!filePath || !existsSync(filePath)) return null;

  try {
    const raw = JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
    const changedPathsSample = normalizeStringArray(raw.changedPathsSample).slice(0, 5);
    const pendingMigrations = normalizeStringArray(raw.pendingMigrations);
    const changedPathCountRaw = raw.changedPathCount;
    const changedPathCount =
      typeof changedPathCountRaw === "number" && Number.isFinite(changedPathCountRaw)
        ? Math.max(0, Math.trunc(changedPathCountRaw))
        : changedPathsSample.length;
    const dirtyRaw = raw.dirty;
    const dirty =
      typeof dirtyRaw === "boolean"
        ? dirtyRaw
        : changedPathCount > 0 || pendingMigrations.length > 0;

    return {
      dirty,
      lastChangedAt: normalizeTimestamp(raw.lastChangedAt),
      changedPathCount,
      changedPathsSample,
      pendingMigrations,
      lastRestartAt: normalizeTimestamp(raw.lastRestartAt),
    };
  } catch {
    return null;
  }
}

export function toDevServerHealthStatus(
  persisted: PersistedDevServerStatus,
  opts: { autoRestartEnabled: boolean; activeRunCount: number },
): DevServerHealthStatus {
  const hasPathChanges = persisted.changedPathCount > 0;
  const hasPendingMigrations = persisted.pendingMigrations.length > 0;
  const reason =
    hasPathChanges && hasPendingMigrations
      ? "backend_changes_and_pending_migrations"
      : hasPendingMigrations
        ? "pending_migrations"
        : hasPathChanges
          ? "backend_changes"
          : null;
  const restartRequired = persisted.dirty || reason !== null;

  return {
    enabled: true,
    restartRequired,
    reason,
    lastChangedAt: persisted.lastChangedAt,
    changedPathCount: persisted.changedPathCount,
    changedPathsSample: persisted.changedPathsSample,
    pendingMigrations: persisted.pendingMigrations,
    autoRestartEnabled: opts.autoRestartEnabled,
    activeRunCount: opts.activeRunCount,
    waitingForIdle: restartRequired && opts.autoRestartEnabled && opts.activeRunCount > 0,
    lastRestartAt: persisted.lastRestartAt,
  };
}